mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-09 09:59:43 -10:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71cd8fa610 | ||
|
|
e9fdcc1285 | ||
|
|
870c995ae2 | ||
|
|
15d7e7c4d1 | ||
|
|
750cdfce0a | ||
|
|
0f128a22b3 | ||
|
|
3296a8f483 | ||
|
|
b77ade2bb9 | ||
|
|
41dc54d736 | ||
|
|
8a0694cb38 | ||
|
|
52c0618bc3 | ||
|
|
e2e1912628 | ||
|
|
36794f1b07 | ||
|
|
ccd8f75b03 | ||
|
|
3c46cf2210 | ||
|
|
edcdfb5937 | ||
|
|
f81efdb38c | ||
|
|
db34ded0a1 | ||
|
|
8a7a92a9b9 | ||
|
|
3eda32f9d7 | ||
|
|
c18c193ea7 | ||
|
|
79a8758448 |
No files matched your search
@@ -48,8 +48,21 @@ jobs:
|
||||
- name: Make Linux (amd64)
|
||||
run: make linux-amd64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test Linux
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
|
||||
macos-build-test:
|
||||
@@ -74,8 +87,21 @@ jobs:
|
||||
- name: Make MacOS (arm64)
|
||||
run: make macos-arm64
|
||||
|
||||
- name: Determine go-tests flags
|
||||
id: go-tests-flags
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_sha="${{ github.event.pull_request.base.sha }}"
|
||||
head_sha="${{ github.sha }}"
|
||||
if git diff --name-only "$base_sha" "$head_sha" | grep -qE '^(implant/|server/gogo/|server/generate/)'; then
|
||||
echo "args=" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "args=--skip-generate" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Test MacOS
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh
|
||||
run: make clean && make && ./sliver-server unpack --force && ./go-tests.sh ${{ steps.go-tests-flags.outputs.args }}
|
||||
|
||||
clients-build:
|
||||
name: Clients Build
|
||||
|
||||
@@ -42,3 +42,4 @@ See the [wiki](https://sliver.sh/docs?name=Compile+from+Source).
|
||||
### License - GPLv3
|
||||
|
||||
Sliver is licensed under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html), some sub-components may have separate licenses. See their respective subdirectories in this project for details.
|
||||
|
||||
+357
-28
@@ -2,7 +2,9 @@ package edit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/alecthomas/chroma/v2/formatters"
|
||||
@@ -37,30 +39,33 @@ type editorResult struct {
|
||||
}
|
||||
|
||||
type editorModel struct {
|
||||
lines [][]rune
|
||||
row int
|
||||
col int
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
mode editorMode
|
||||
command string
|
||||
pending rune
|
||||
dirty bool
|
||||
filename string
|
||||
showLineNumbers bool
|
||||
syntaxName string
|
||||
lexer chroma.Lexer
|
||||
formatter chroma.Formatter
|
||||
style *chroma.Style
|
||||
highlighted []string
|
||||
highlightOn bool
|
||||
highlightDirty bool
|
||||
action exitAction
|
||||
forceQuit bool
|
||||
message string
|
||||
clearMessage bool
|
||||
lines [][]rune
|
||||
row int
|
||||
col int
|
||||
top int
|
||||
left int
|
||||
width int
|
||||
height int
|
||||
mode editorMode
|
||||
command string
|
||||
commandPrefix rune
|
||||
pending rune
|
||||
dirty bool
|
||||
filename string
|
||||
showLineNumbers bool
|
||||
syntaxName string
|
||||
lexer chroma.Lexer
|
||||
formatter chroma.Formatter
|
||||
style *chroma.Style
|
||||
highlighted []string
|
||||
highlightOn bool
|
||||
highlightDirty bool
|
||||
lastSearch string
|
||||
lastSearchForward bool
|
||||
action exitAction
|
||||
forceQuit bool
|
||||
message string
|
||||
clearMessage bool
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -76,6 +81,7 @@ func newEditorModel(content, filename string, lexer chroma.Lexer, syntaxName str
|
||||
mode: modeNormal,
|
||||
filename: filename,
|
||||
showLineNumbers: showLineNumbers,
|
||||
commandPrefix: ':',
|
||||
}
|
||||
model.setSyntax(lexer, syntaxName)
|
||||
return model
|
||||
@@ -175,10 +181,16 @@ func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
case tea.KeyEnter:
|
||||
m.executeCommand(strings.TrimSpace(m.command))
|
||||
if m.commandPrefix == '/' {
|
||||
m.executeSearch(strings.TrimSpace(m.command))
|
||||
} else {
|
||||
m.executeCommand(strings.TrimSpace(m.command))
|
||||
}
|
||||
m.command = ""
|
||||
m.mode = modeNormal
|
||||
m.commandPrefix = ':'
|
||||
if m.action != actionNone {
|
||||
return m, tea.Quit
|
||||
}
|
||||
@@ -276,6 +288,14 @@ func (m *editorModel) handleNormalRune(r rune) bool {
|
||||
m.moveUp()
|
||||
case 'l':
|
||||
m.moveRight()
|
||||
case 'w':
|
||||
m.moveWordForward()
|
||||
case 'b':
|
||||
m.moveWordBackward()
|
||||
case 'e':
|
||||
m.moveWordEnd()
|
||||
case '^':
|
||||
m.moveLineFirstNonWhitespace()
|
||||
case '0':
|
||||
m.moveLineStart()
|
||||
case '$':
|
||||
@@ -303,15 +323,31 @@ func (m *editorModel) handleNormalRune(r rune) bool {
|
||||
m.pending = 'g'
|
||||
case 'G':
|
||||
m.gotoBottom()
|
||||
case 'n':
|
||||
m.searchNext(true)
|
||||
case 'N':
|
||||
m.searchNext(false)
|
||||
case ':':
|
||||
m.mode = modeCommand
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
case '/':
|
||||
m.mode = modeCommand
|
||||
m.command = ""
|
||||
m.commandPrefix = '/'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *editorModel) executeCommand(cmd string) {
|
||||
if isDigits(cmd) {
|
||||
target, err := strconv.Atoi(cmd)
|
||||
if err == nil {
|
||||
m.gotoLine(target)
|
||||
return
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(cmd) {
|
||||
case "":
|
||||
return
|
||||
@@ -408,15 +444,19 @@ func (m *editorModel) statusLine() string {
|
||||
|
||||
func (m *editorModel) commandLine() string {
|
||||
if m.mode == modeCommand {
|
||||
return ":" + m.command
|
||||
prefix := ":"
|
||||
if m.commandPrefix == '/' {
|
||||
prefix = "/"
|
||||
}
|
||||
return prefix + m.command
|
||||
}
|
||||
if m.message != "" {
|
||||
return m.message
|
||||
}
|
||||
if m.mode == modeInsert {
|
||||
return "ESC: normal :wq save+quit :q quit :n line numbers"
|
||||
return "ESC: normal / search :wq save+quit :q quit :n line numbers :<num> goto line"
|
||||
}
|
||||
return "i: insert h/j/k/l: move :wq save+quit :q quit :n line numbers"
|
||||
return "i: insert h/j/k/l: move / search :wq save+quit :q quit :n line numbers :<num> goto line"
|
||||
}
|
||||
|
||||
func (m *editorModel) content() string {
|
||||
@@ -551,6 +591,18 @@ func (m *editorModel) moveLineStart() {
|
||||
m.col = 0
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineFirstNonWhitespace() {
|
||||
line := m.lines[m.row]
|
||||
col := 0
|
||||
for col < len(line) {
|
||||
if line[col] != ' ' && line[col] != '\t' {
|
||||
break
|
||||
}
|
||||
col++
|
||||
}
|
||||
m.col = col
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineEnd() {
|
||||
m.col = len(m.lines[m.row])
|
||||
}
|
||||
@@ -570,6 +622,115 @@ func (m *editorModel) gotoBottom() {
|
||||
m.clampCol()
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoLine(lineNumber int) {
|
||||
if lineNumber < 1 {
|
||||
lineNumber = 1
|
||||
}
|
||||
if len(m.lines) == 0 {
|
||||
m.row = 0
|
||||
m.col = 0
|
||||
return
|
||||
}
|
||||
if lineNumber > len(m.lines) {
|
||||
lineNumber = len(m.lines)
|
||||
}
|
||||
m.row = lineNumber - 1
|
||||
m.clampCol()
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
|
||||
func (m *editorModel) moveWordForward() {
|
||||
row := m.row
|
||||
col := m.col
|
||||
for {
|
||||
if row >= len(m.lines) {
|
||||
return
|
||||
}
|
||||
line := m.lines[row]
|
||||
if col >= len(line) {
|
||||
row++
|
||||
col = 0
|
||||
continue
|
||||
}
|
||||
if isWordChar(line[col]) {
|
||||
for col < len(line) && isWordChar(line[col]) {
|
||||
col++
|
||||
}
|
||||
}
|
||||
for col < len(line) && !isWordChar(line[col]) {
|
||||
col++
|
||||
}
|
||||
if col < len(line) {
|
||||
m.row = row
|
||||
m.col = col
|
||||
return
|
||||
}
|
||||
row++
|
||||
col = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveWordBackward() {
|
||||
row := m.row
|
||||
col := m.col - 1
|
||||
for {
|
||||
if row < 0 {
|
||||
return
|
||||
}
|
||||
line := m.lines[row]
|
||||
if col >= len(line) {
|
||||
col = len(line) - 1
|
||||
}
|
||||
for col >= 0 && (col >= len(line) || !isWordChar(line[col])) {
|
||||
col--
|
||||
}
|
||||
for col >= 0 && isWordChar(line[col]) {
|
||||
col--
|
||||
}
|
||||
if col+1 >= 0 && col+1 < len(line) {
|
||||
m.row = row
|
||||
m.col = col + 1
|
||||
return
|
||||
}
|
||||
row--
|
||||
if row >= 0 {
|
||||
col = len(m.lines[row]) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveWordEnd() {
|
||||
row := m.row
|
||||
col := m.col
|
||||
for {
|
||||
if row >= len(m.lines) {
|
||||
return
|
||||
}
|
||||
line := m.lines[row]
|
||||
if col >= len(line) {
|
||||
row++
|
||||
col = 0
|
||||
continue
|
||||
}
|
||||
if isWordChar(line[col]) {
|
||||
for col+1 < len(line) && isWordChar(line[col+1]) {
|
||||
col++
|
||||
}
|
||||
m.row = row
|
||||
m.col = col
|
||||
return
|
||||
}
|
||||
for col < len(line) && !isWordChar(line[col]) {
|
||||
col++
|
||||
}
|
||||
if col < len(line) {
|
||||
continue
|
||||
}
|
||||
row++
|
||||
col = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) clampCol() {
|
||||
lineLen := len(m.lines[m.row])
|
||||
if m.col > lineLen {
|
||||
@@ -763,6 +924,104 @@ func (m *editorModel) applyCursor(rendered, highlighted string, line []rune, sta
|
||||
return left + cursorStyle.Render(string(line[cursor])) + right
|
||||
}
|
||||
|
||||
func (m *editorModel) executeSearch(pattern string) {
|
||||
if pattern == "" {
|
||||
if m.lastSearch == "" {
|
||||
return
|
||||
}
|
||||
pattern = m.lastSearch
|
||||
}
|
||||
m.lastSearch = pattern
|
||||
m.lastSearchForward = true
|
||||
if !m.searchNext(true) {
|
||||
m.message = fmt.Sprintf("Pattern not found: %s", pattern)
|
||||
m.clearMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) searchNext(forward bool) bool {
|
||||
pattern := m.lastSearch
|
||||
if pattern == "" {
|
||||
m.message = "No previous search"
|
||||
m.clearMessage = true
|
||||
return false
|
||||
}
|
||||
row, col, ok := m.findMatch(pattern, forward)
|
||||
if !ok {
|
||||
m.message = fmt.Sprintf("Pattern not found: %s", pattern)
|
||||
m.clearMessage = true
|
||||
return false
|
||||
}
|
||||
m.row = row
|
||||
m.col = col
|
||||
m.lastSearchForward = forward
|
||||
m.ensureCursorVisible()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *editorModel) findMatch(pattern string, forward bool) (int, int, bool) {
|
||||
if pattern == "" {
|
||||
return 0, 0, false
|
||||
}
|
||||
pat := []rune(pattern)
|
||||
if forward {
|
||||
return m.findForward(pat)
|
||||
}
|
||||
return m.findBackward(pat)
|
||||
}
|
||||
|
||||
func (m *editorModel) findForward(pattern []rune) (int, int, bool) {
|
||||
row := m.row
|
||||
col := m.col + 1
|
||||
for r := row; r < len(m.lines); r++ {
|
||||
line := m.lines[r]
|
||||
start := 0
|
||||
if r == row {
|
||||
start = col
|
||||
}
|
||||
if idx := indexOfRunes(line, pattern, start); idx >= 0 {
|
||||
return r, idx, true
|
||||
}
|
||||
}
|
||||
for r := 0; r <= row && r < len(m.lines); r++ {
|
||||
line := m.lines[r]
|
||||
end := len(line)
|
||||
if r == row {
|
||||
end = min(end, col)
|
||||
}
|
||||
if idx := indexOfRunes(line, pattern, 0); idx >= 0 && idx < end {
|
||||
return r, idx, true
|
||||
}
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func (m *editorModel) findBackward(pattern []rune) (int, int, bool) {
|
||||
row := m.row
|
||||
col := m.col - 1
|
||||
for r := row; r >= 0; r-- {
|
||||
line := m.lines[r]
|
||||
end := len(line)
|
||||
if r == row {
|
||||
end = col + 1
|
||||
}
|
||||
if idx := lastIndexOfRunes(line, pattern, end); idx >= 0 {
|
||||
return r, idx, true
|
||||
}
|
||||
}
|
||||
for r := len(m.lines) - 1; r >= row && r >= 0; r-- {
|
||||
line := m.lines[r]
|
||||
end := len(line)
|
||||
if r == row {
|
||||
end = len(line)
|
||||
}
|
||||
if idx := lastIndexOfRunes(line, pattern, end); idx >= 0 {
|
||||
return r, idx, true
|
||||
}
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func splitLines(content string) [][]rune {
|
||||
parts := strings.Split(content, "\n")
|
||||
if len(parts) == 0 {
|
||||
@@ -783,6 +1042,76 @@ func popRune(value string) string {
|
||||
return string(runes[:len(runes)-1])
|
||||
}
|
||||
|
||||
func isDigits(value string) bool {
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isWordChar(r rune) bool {
|
||||
return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}
|
||||
|
||||
func indexOfRunes(haystack, needle []rune, start int) int {
|
||||
if len(needle) == 0 {
|
||||
return -1
|
||||
}
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if start >= len(haystack) {
|
||||
return -1
|
||||
}
|
||||
for i := start; i+len(needle) <= len(haystack); i++ {
|
||||
match := true
|
||||
for j := range needle {
|
||||
if haystack[i+j] != needle[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func lastIndexOfRunes(haystack, needle []rune, end int) int {
|
||||
if len(needle) == 0 {
|
||||
return -1
|
||||
}
|
||||
if end > len(haystack) {
|
||||
end = len(haystack)
|
||||
}
|
||||
for i := end - len(needle); i >= 0; i-- {
|
||||
match := true
|
||||
for j := range needle {
|
||||
if haystack[i+j] != needle[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
|
||||
@@ -19,6 +19,8 @@ package filesystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
@@ -28,14 +30,77 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const chtimesDefaultLayout = "2006-01-02 15:04:05"
|
||||
|
||||
type chtimesTimeFormat struct {
|
||||
name string
|
||||
parse func(string) (int64, error)
|
||||
}
|
||||
|
||||
func chtimesParseUnixSeconds(value string) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func chtimesParseUnixMillis(value string) (int64, error) {
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.UnixMilli(parsed).Unix(), nil
|
||||
}
|
||||
|
||||
func chtimesParseLayout(layout string) func(string) (int64, error) {
|
||||
return func(value string) (int64, error) {
|
||||
parsed, err := time.Parse(layout, value)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return parsed.Unix(), nil
|
||||
}
|
||||
}
|
||||
|
||||
func chtimesFormatFromFlags(cmd *cobra.Command) (chtimesTimeFormat, error) {
|
||||
formatFlags := []struct {
|
||||
flag string
|
||||
format chtimesTimeFormat
|
||||
}{
|
||||
{flag: "unix", format: chtimesTimeFormat{name: "unix", parse: chtimesParseUnixSeconds}},
|
||||
{flag: "unix-ms", format: chtimesTimeFormat{name: "unix-ms", parse: chtimesParseUnixMillis}},
|
||||
{flag: "rfc3339", format: chtimesTimeFormat{name: "rfc3339", parse: chtimesParseLayout(time.RFC3339)}},
|
||||
{flag: "rfc1123", format: chtimesTimeFormat{name: "rfc1123", parse: chtimesParseLayout(time.RFC1123)}},
|
||||
}
|
||||
|
||||
var selected *chtimesTimeFormat
|
||||
selectedFlag := ""
|
||||
for _, candidate := range formatFlags {
|
||||
enabled, _ := cmd.Flags().GetBool(candidate.flag)
|
||||
if !enabled {
|
||||
continue
|
||||
}
|
||||
if selected != nil {
|
||||
return chtimesTimeFormat{}, fmt.Errorf("only one time format flag can be used (--%s and --%s are both set)", selectedFlag, candidate.flag)
|
||||
}
|
||||
selected = &candidate.format
|
||||
selectedFlag = candidate.flag
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
return chtimesTimeFormat{name: "datetime", parse: chtimesParseLayout(chtimesDefaultLayout)}, nil
|
||||
}
|
||||
|
||||
return *selected, nil
|
||||
}
|
||||
|
||||
// ChtimesCmd - Change the access and modified time of a file on the remote file system.
|
||||
func ChtimesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
// DateTime layout (https://pkg.go.dev/time)
|
||||
layout := "2006-01-02 15:04:05"
|
||||
filePath := args[0]
|
||||
|
||||
if filePath == "" {
|
||||
@@ -43,33 +108,35 @@ func ChtimesCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
return
|
||||
}
|
||||
|
||||
atime := args[1]
|
||||
format, err := chtimesFormatFromFlags(cmd)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
atime := args[1]
|
||||
if atime == "" {
|
||||
con.PrintErrorf("Missing parameter: Last accessed time id\n")
|
||||
return
|
||||
}
|
||||
|
||||
t_a, err := time.Parse(layout, atime)
|
||||
unixAtime, err := format.parse(atime)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Invalid access time (%s): %s\n", format.name, err)
|
||||
return
|
||||
}
|
||||
unixAtime := t_a.Unix()
|
||||
|
||||
mtime := args[2]
|
||||
|
||||
if mtime == "" {
|
||||
con.PrintErrorf("Missing parameter: Last modified time id\n")
|
||||
return
|
||||
}
|
||||
|
||||
t_b, err := time.Parse(layout, mtime)
|
||||
unixMtime, err := format.parse(mtime)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
con.PrintErrorf("Invalid modified time (%s): %s\n", format.name, err)
|
||||
return
|
||||
}
|
||||
unixMtime := t_b.Unix()
|
||||
|
||||
chtimes, err := con.Rpc.Chtimes(context.Background(), &sliverpb.ChtimesReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
|
||||
@@ -57,6 +57,7 @@ var (
|
||||
consts.PwdStr: pwdHelp,
|
||||
consts.CatStr: catHelp,
|
||||
consts.EditStr: editHelp,
|
||||
consts.HexEditStr: hexEditHelp,
|
||||
consts.DownloadStr: downloadHelp,
|
||||
consts.GrepStr: grepHelp,
|
||||
consts.HeadStr: headHelp,
|
||||
@@ -64,6 +65,7 @@ var (
|
||||
consts.UploadStr: uploadHelp,
|
||||
consts.MkdirStr: mkdirHelp,
|
||||
consts.RmStr: rmHelp,
|
||||
consts.ChtimesStr: chtimesHelp,
|
||||
consts.ProcdumpStr: procdumpHelp,
|
||||
consts.ElevateStr: elevateHelp,
|
||||
consts.RunAsStr: runAsHelp,
|
||||
@@ -299,6 +301,24 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
|
||||
rmHelp = `[[.Bold]]Command:[[.Normal]] rm [remote path]
|
||||
[[.Bold]]About:[[.Normal]] Delete a remote file or directory.`
|
||||
|
||||
chtimesHelp = `[[.Bold]]Command:[[.Normal]] chtimes [--unix | --unix-ms | --rfc3339 | --rfc1123] <path> <atime> <mtime>
|
||||
[[.Bold]]About:[[.Normal]] Change access and modification times on a file (timestomp).
|
||||
|
||||
[[.Bold]][[.Underline]]Time Formats[[.Normal]]
|
||||
Default (datetime): "2006-01-02 15:04:05"
|
||||
--unix: 1704067200
|
||||
--unix-ms: 1704067200000
|
||||
--rfc3339: 2024-01-01T00:00:00Z
|
||||
--rfc1123: Mon, 02 Jan 2006 15:04:05 MST
|
||||
|
||||
[[.Bold]][[.Underline]]Examples[[.Normal]]
|
||||
chtimes /tmp/file "2024-01-01 12:34:56" "2024-01-01 12:35:56"
|
||||
chtimes --unix /tmp/file 1704112496 1704112556
|
||||
chtimes --unix-ms /tmp/file 1704112496000 1704112556000
|
||||
chtimes --rfc3339 /tmp/file 2024-01-01T12:34:56Z 2024-01-01T12:35:56Z
|
||||
chtimes --rfc1123 /tmp/file "Mon, 02 Jan 2006 15:04:05 MST" "Mon, 02 Jan 2006 16:04:05 MST"
|
||||
`
|
||||
|
||||
catHelp = `[[.Bold]]Command:[[.Normal]] cat <remote path>
|
||||
[[.Bold]]About:[[.Normal]] Cat a remote file to stdout.`
|
||||
|
||||
@@ -308,6 +328,12 @@ On Windows, escaping is disabled. Instead, '\\' is treated as path separator.
|
||||
[[.Bold]]Syntax:[[.Normal]] Use --syntax <lexer> to force a lexer or --syntax-select to pick one interactively.
|
||||
[[.Bold]]Line Numbers:[[.Normal]] Use --line-numbers to start with line numbers on; toggle with :n.`
|
||||
|
||||
hexEditHelp = `[[.Bold]]Command:[[.Normal]] hex-edit [--max-size <size>] [--offset <byte offset>] <remote path>
|
||||
[[.Bold]]About:[[.Normal]] Download a remote file and edit it in a built-in hex editor, then optionally upload changes.
|
||||
[[.Bold]]Keys:[[.Normal]] Normal mode uses vim-like navigation (h/j/k/l). Use i for hex edit mode, a for ASCII edit mode, :wq to save+quit or :q to quit.
|
||||
[[.Bold]]Size Limit:[[.Normal]] Default max size is 8MB. Use --max-size to override (e.g., 512KB, 12MB).
|
||||
[[.Bold]]Offset:[[.Normal]] Use --offset to jump to a specific byte offset when opening the file.`
|
||||
|
||||
downloadHelp = `[[.Bold]]Command:[[.Normal]] download [remote src] <local dst>
|
||||
[[.Bold]]About:[[.Normal]] Download a file or directory from the remote system. Directories will be downloaded as a gzipped TAR file.
|
||||
[[.Bold]][[.Underline]]Filters[[.Normal]]
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// Commands returns the hex-edit command.
|
||||
func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
hexEditCmd := &cobra.Command{
|
||||
Use: consts.HexEditStr + " <remote path>",
|
||||
Short: "Hex edit a remote file",
|
||||
Long: help.GetHelpFor([]string{consts.HexEditStr}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
GroupID: consts.FilesystemHelpGroup,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
HexEditCmd(cmd, con, args)
|
||||
},
|
||||
}
|
||||
|
||||
flags.Bind("", false, hexEditCmd, func(f *pflag.FlagSet) {
|
||||
f.String("max-size", "8MB", "maximum file size to load (e.g. 8MB, 512KB)")
|
||||
f.Int64("offset", 0, "byte offset to jump to (decimal or 0x...)")
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
})
|
||||
|
||||
carapace.Gen(hexEditCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to the remote file to hex edit"),
|
||||
)
|
||||
|
||||
return []*cobra.Command{hexEditCmd}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type editorMode int
|
||||
|
||||
const (
|
||||
modeNormal editorMode = iota
|
||||
modeHexEdit
|
||||
modeASCIIEdit
|
||||
modeCommand
|
||||
)
|
||||
|
||||
type exitAction int
|
||||
|
||||
const (
|
||||
actionNone exitAction = iota
|
||||
actionQuit
|
||||
actionSaveQuit
|
||||
)
|
||||
|
||||
const bytesPerLine = 16
|
||||
|
||||
type editorResult struct {
|
||||
Data []byte
|
||||
Modified bool
|
||||
Action exitAction
|
||||
Force bool
|
||||
}
|
||||
|
||||
type editorModel struct {
|
||||
data []byte
|
||||
original []byte
|
||||
modified []bool
|
||||
modifiedCount int
|
||||
cursor int
|
||||
top int
|
||||
width int
|
||||
height int
|
||||
mode editorMode
|
||||
command string
|
||||
commandPrefix rune
|
||||
pending rune
|
||||
dirty bool
|
||||
forceQuit bool
|
||||
action exitAction
|
||||
filename string
|
||||
message string
|
||||
clearMessage bool
|
||||
nibbleSet bool
|
||||
nibbleValue byte
|
||||
}
|
||||
|
||||
var (
|
||||
cursorStyle = lipgloss.NewStyle().Reverse(true)
|
||||
modifiedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215"))
|
||||
modifiedCursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("215")).Reverse(true)
|
||||
statusStyle = lipgloss.NewStyle().Reverse(true)
|
||||
lineStyle = lipgloss.NewStyle()
|
||||
)
|
||||
|
||||
func newEditorModel(data []byte, filename string, offset int) *editorModel {
|
||||
copyData := append([]byte(nil), data...)
|
||||
model := &editorModel{
|
||||
data: copyData,
|
||||
original: append([]byte(nil), data...),
|
||||
modified: make([]bool, len(copyData)),
|
||||
mode: modeNormal,
|
||||
filename: filename,
|
||||
commandPrefix: ':',
|
||||
}
|
||||
if offset > 0 && offset < len(copyData) {
|
||||
model.cursor = offset
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
func (m *editorModel) Init() tea.Cmd {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
m.width = w
|
||||
m.height = h
|
||||
} else {
|
||||
m.width = 80
|
||||
m.height = 24
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
if m.clearMessage {
|
||||
m.message = ""
|
||||
m.clearMessage = false
|
||||
}
|
||||
if msg.Type == tea.KeyCtrlC {
|
||||
m.action = actionQuit
|
||||
return m, tea.Quit
|
||||
}
|
||||
switch m.mode {
|
||||
case modeCommand:
|
||||
return m.handleCommand(msg)
|
||||
case modeHexEdit:
|
||||
return m.handleHexEdit(msg)
|
||||
case modeASCIIEdit:
|
||||
return m.handleASCIIEdit(msg)
|
||||
default:
|
||||
return m.handleNormal(msg)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) View() string {
|
||||
if m.width == 0 || m.height == 0 {
|
||||
if w, h, err := term.GetSize(0); err == nil && w > 0 && h > 0 {
|
||||
m.width = w
|
||||
m.height = h
|
||||
} else {
|
||||
m.width = 80
|
||||
m.height = 24
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
|
||||
textHeight := m.textHeight()
|
||||
var b strings.Builder
|
||||
for i := 0; i < textHeight; i++ {
|
||||
lineIndex := m.top + i
|
||||
b.WriteString(m.renderLine(lineIndex))
|
||||
if i < textHeight-1 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
if m.height < 2 {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(statusStyle.Width(m.width).Render(m.statusLine()))
|
||||
|
||||
if m.height < 3 {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
b.WriteString(lineStyle.Width(m.width).Render(m.commandLine()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m *editorModel) result() editorResult {
|
||||
data := append([]byte(nil), m.data...)
|
||||
return editorResult{
|
||||
Data: data,
|
||||
Modified: m.dirty,
|
||||
Action: m.action,
|
||||
Force: m.forceQuit,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) handleCommand(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
case tea.KeyEnter:
|
||||
m.executeCommand(strings.TrimSpace(m.command))
|
||||
m.command = ""
|
||||
m.mode = modeNormal
|
||||
m.commandPrefix = ':'
|
||||
if m.action != actionNone {
|
||||
return m, tea.Quit
|
||||
}
|
||||
case tea.KeyBackspace:
|
||||
m.command = popRune(m.command)
|
||||
case tea.KeyRunes:
|
||||
m.command += string(msg.Runes)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleHexEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
m.nibbleSet = false
|
||||
case tea.KeyBackspace:
|
||||
if m.nibbleSet {
|
||||
m.nibbleSet = false
|
||||
return m, nil
|
||||
}
|
||||
m.moveLeft()
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
m.nibbleSet = false
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
val, ok := hexNibble(r)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !m.nibbleSet {
|
||||
m.nibbleValue = val << 4
|
||||
m.nibbleSet = true
|
||||
continue
|
||||
}
|
||||
m.setByte(m.nibbleValue | val)
|
||||
m.nibbleSet = false
|
||||
m.moveRight()
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleASCIIEdit(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.mode = modeNormal
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
if r < 0 || r > 0xFF {
|
||||
continue
|
||||
}
|
||||
m.setByte(byte(r))
|
||||
m.moveRight()
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleNormal(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.pending = 0
|
||||
case tea.KeyLeft:
|
||||
m.moveLeft()
|
||||
case tea.KeyRight:
|
||||
m.moveRight()
|
||||
case tea.KeyUp:
|
||||
m.moveUp()
|
||||
case tea.KeyDown:
|
||||
m.moveDown()
|
||||
case tea.KeyRunes:
|
||||
for _, r := range msg.Runes {
|
||||
if m.handleNormalRune(r) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
m.ensureCursorVisible()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *editorModel) handleNormalRune(r rune) bool {
|
||||
if m.pending != 0 {
|
||||
handled := true
|
||||
switch m.pending {
|
||||
case 'g':
|
||||
if r == 'g' {
|
||||
m.gotoTop()
|
||||
} else {
|
||||
handled = false
|
||||
}
|
||||
default:
|
||||
handled = false
|
||||
}
|
||||
m.pending = 0
|
||||
if handled {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
switch r {
|
||||
case 'h':
|
||||
m.moveLeft()
|
||||
case 'j':
|
||||
m.moveDown()
|
||||
case 'k':
|
||||
m.moveUp()
|
||||
case 'l':
|
||||
m.moveRight()
|
||||
case '0':
|
||||
m.moveLineStart()
|
||||
case '$':
|
||||
m.moveLineEnd()
|
||||
case 'i':
|
||||
m.mode = modeHexEdit
|
||||
m.nibbleSet = false
|
||||
case 'a':
|
||||
m.mode = modeASCIIEdit
|
||||
case 'x':
|
||||
m.setByte(0x00)
|
||||
case 'g':
|
||||
m.pending = 'g'
|
||||
case 'G':
|
||||
m.gotoBottom()
|
||||
case ':':
|
||||
m.mode = modeCommand
|
||||
m.command = ""
|
||||
m.commandPrefix = ':'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *editorModel) executeCommand(cmd string) {
|
||||
if cmd == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if isNumericOffset(cmd) {
|
||||
if value, err := strconv.ParseInt(cmd, 0, 64); err == nil {
|
||||
m.gotoOffset(int(value))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch strings.ToLower(cmd) {
|
||||
case "q":
|
||||
m.action = actionQuit
|
||||
case "q!":
|
||||
m.action = actionQuit
|
||||
m.forceQuit = true
|
||||
case "wq", "x":
|
||||
m.action = actionSaveQuit
|
||||
case "w":
|
||||
m.message = "Use :wq to upload and exit"
|
||||
m.clearMessage = true
|
||||
default:
|
||||
m.message = fmt.Sprintf("Unknown command: %s", cmd)
|
||||
m.clearMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) renderLine(lineIndex int) string {
|
||||
totalWidth := m.textWidth()
|
||||
if totalWidth <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lineIndex >= m.totalLines() {
|
||||
return lineStyle.Width(totalWidth).Render("~")
|
||||
}
|
||||
|
||||
lineStart := lineIndex * bytesPerLine
|
||||
hexCells := make([]string, bytesPerLine)
|
||||
asciiCells := make([]string, bytesPerLine)
|
||||
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
idx := lineStart + i
|
||||
if idx >= len(m.data) {
|
||||
hexCells[i] = " "
|
||||
asciiCells[i] = " "
|
||||
continue
|
||||
}
|
||||
byteVal := m.data[idx]
|
||||
hexRaw := fmt.Sprintf("%02x", byteVal)
|
||||
if idx == m.cursor && m.mode == modeHexEdit && m.nibbleSet {
|
||||
hexRaw = string([]byte{hexChar(m.nibbleValue >> 4), '_'})
|
||||
}
|
||||
asciiRaw := string(printableByte(byteVal))
|
||||
isModified := m.isModified(idx)
|
||||
if idx == m.cursor {
|
||||
if isModified {
|
||||
hexCells[i] = modifiedCursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedCursorStyle.Render(asciiRaw)
|
||||
} else {
|
||||
hexCells[i] = cursorStyle.Render(hexRaw)
|
||||
asciiCells[i] = cursorStyle.Render(asciiRaw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if isModified {
|
||||
hexCells[i] = modifiedStyle.Render(hexRaw)
|
||||
asciiCells[i] = modifiedStyle.Render(asciiRaw)
|
||||
continue
|
||||
}
|
||||
hexCells[i] = hexRaw
|
||||
asciiCells[i] = asciiRaw
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(fmt.Sprintf("%08x", lineStart))
|
||||
b.WriteString(": ")
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
if i == 8 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(hexCells[i])
|
||||
if i < bytesPerLine-1 {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
b.WriteString(" |")
|
||||
for i := 0; i < bytesPerLine; i++ {
|
||||
b.WriteString(asciiCells[i])
|
||||
}
|
||||
b.WriteString("|")
|
||||
|
||||
return lineStyle.Width(totalWidth).Render(b.String())
|
||||
}
|
||||
|
||||
func (m *editorModel) statusLine() string {
|
||||
mode := "NORMAL"
|
||||
switch m.mode {
|
||||
case modeHexEdit:
|
||||
mode = "HEX"
|
||||
case modeASCIIEdit:
|
||||
mode = "ASCII"
|
||||
case modeCommand:
|
||||
mode = "COMMAND"
|
||||
}
|
||||
fileName := baseName(m.filename)
|
||||
if fileName == "" {
|
||||
fileName = "[No Name]"
|
||||
}
|
||||
modified := ""
|
||||
if m.dirty {
|
||||
modified = " [+]"
|
||||
}
|
||||
cursor := m.cursor
|
||||
return fmt.Sprintf(" %s %s Off 0x%08x (%d) Size %s%s", mode, fileName, cursor, cursor, util.ByteCountBinary(int64(len(m.data))), modified)
|
||||
}
|
||||
|
||||
func (m *editorModel) commandLine() string {
|
||||
if m.mode == modeCommand {
|
||||
return ":" + m.command
|
||||
}
|
||||
if m.message != "" {
|
||||
return m.message
|
||||
}
|
||||
switch m.mode {
|
||||
case modeHexEdit:
|
||||
return "ESC: normal hex: 0-9/a-f :wq save+quit :q quit"
|
||||
case modeASCIIEdit:
|
||||
return "ESC: normal type to edit bytes :wq save+quit :q quit"
|
||||
default:
|
||||
return "i: hex edit a: ascii edit h/j/k/l move x zero :wq save+quit :q quit"
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) textHeight() int {
|
||||
if m.height <= 2 {
|
||||
return 1
|
||||
}
|
||||
return m.height - 2
|
||||
}
|
||||
|
||||
func (m *editorModel) textWidth() int {
|
||||
if m.width <= 0 {
|
||||
return 1
|
||||
}
|
||||
return m.width
|
||||
}
|
||||
|
||||
func (m *editorModel) totalLines() int {
|
||||
if len(m.data) == 0 {
|
||||
return 1
|
||||
}
|
||||
return (len(m.data) + bytesPerLine - 1) / bytesPerLine
|
||||
}
|
||||
|
||||
func (m *editorModel) ensureCursorVisible() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
m.top = 0
|
||||
return
|
||||
}
|
||||
if m.cursor < 0 {
|
||||
m.cursor = 0
|
||||
}
|
||||
if m.cursor >= len(m.data) {
|
||||
m.cursor = len(m.data) - 1
|
||||
}
|
||||
|
||||
line := m.cursor / bytesPerLine
|
||||
height := m.textHeight()
|
||||
if line < m.top {
|
||||
m.top = line
|
||||
} else if line >= m.top+height {
|
||||
m.top = line - height + 1
|
||||
}
|
||||
if m.top < 0 {
|
||||
m.top = 0
|
||||
}
|
||||
maxTop := m.totalLines() - height
|
||||
if maxTop < 0 {
|
||||
maxTop = 0
|
||||
}
|
||||
if m.top > maxTop {
|
||||
m.top = maxTop
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLeft() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveRight() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor < len(m.data)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveUp() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor >= bytesPerLine {
|
||||
m.cursor -= bytesPerLine
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveDown() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
if m.cursor+bytesPerLine < len(m.data) {
|
||||
m.cursor += bytesPerLine
|
||||
}
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineStart() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
line := m.cursor / bytesPerLine
|
||||
m.cursor = line * bytesPerLine
|
||||
}
|
||||
|
||||
func (m *editorModel) moveLineEnd() {
|
||||
if len(m.data) == 0 {
|
||||
return
|
||||
}
|
||||
line := m.cursor / bytesPerLine
|
||||
end := (line+1)*bytesPerLine - 1
|
||||
if end >= len(m.data) {
|
||||
end = len(m.data) - 1
|
||||
}
|
||||
m.cursor = end
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoTop() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
return
|
||||
}
|
||||
m.cursor = 0
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoBottom() {
|
||||
if len(m.data) == 0 {
|
||||
m.cursor = 0
|
||||
return
|
||||
}
|
||||
m.cursor = len(m.data) - 1
|
||||
}
|
||||
|
||||
func (m *editorModel) gotoOffset(offset int) {
|
||||
if len(m.data) == 0 {
|
||||
m.message = "Empty file"
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
if offset < 0 || offset >= len(m.data) {
|
||||
m.message = fmt.Sprintf("Offset out of range: %d", offset)
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
m.cursor = offset
|
||||
m.ensureCursorVisible()
|
||||
}
|
||||
|
||||
func (m *editorModel) setByte(value byte) {
|
||||
if len(m.data) == 0 {
|
||||
m.message = "Empty file"
|
||||
m.clearMessage = true
|
||||
return
|
||||
}
|
||||
if m.cursor >= len(m.data) || m.cursor >= len(m.original) {
|
||||
return
|
||||
}
|
||||
wasModified := m.modified[m.cursor]
|
||||
orig := m.original[m.cursor]
|
||||
m.data[m.cursor] = value
|
||||
if value == orig {
|
||||
if wasModified {
|
||||
m.modified[m.cursor] = false
|
||||
if m.modifiedCount > 0 {
|
||||
m.modifiedCount--
|
||||
}
|
||||
}
|
||||
} else if !wasModified {
|
||||
m.modified[m.cursor] = true
|
||||
m.modifiedCount++
|
||||
}
|
||||
m.dirty = m.modifiedCount > 0
|
||||
}
|
||||
|
||||
func printableByte(b byte) byte {
|
||||
if b >= 32 && b <= 126 {
|
||||
return b
|
||||
}
|
||||
return '.'
|
||||
}
|
||||
|
||||
func (m *editorModel) isModified(idx int) bool {
|
||||
if idx < 0 || idx >= len(m.modified) {
|
||||
return false
|
||||
}
|
||||
return m.modified[idx]
|
||||
}
|
||||
|
||||
func hexChar(n byte) byte {
|
||||
if n < 10 {
|
||||
return '0' + n
|
||||
}
|
||||
return 'a' + (n - 10)
|
||||
}
|
||||
|
||||
func hexNibble(r rune) (byte, bool) {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
return byte(r - '0'), true
|
||||
case r >= 'a' && r <= 'f':
|
||||
return byte(r-'a') + 10, true
|
||||
case r >= 'A' && r <= 'F':
|
||||
return byte(r-'A') + 10, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func isNumericOffset(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(value), "0x") {
|
||||
_, err := strconv.ParseInt(value, 0, 64)
|
||||
return err == nil
|
||||
}
|
||||
for _, r := range value {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func popRune(value string) string {
|
||||
runes := []rune(value)
|
||||
if len(runes) == 0 {
|
||||
return value
|
||||
}
|
||||
return string(runes[:len(runes)-1])
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
clean := strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(clean, "/"); idx != -1 {
|
||||
return clean[idx+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package hexedit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/filesystem"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
beaconPollInterval = 1 * time.Second
|
||||
)
|
||||
|
||||
// HexEditCmd downloads a remote file, opens it in a hex editor, and optionally uploads changes.
|
||||
func HexEditCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
session, beacon := con.ActiveTarget.GetInteractive()
|
||||
if session == nil && beacon == nil {
|
||||
return
|
||||
}
|
||||
|
||||
remotePath := strings.TrimSpace(args[0])
|
||||
if remotePath == "" {
|
||||
con.PrintErrorf("Missing parameter: remote path\n")
|
||||
return
|
||||
}
|
||||
|
||||
maxSizeStr, _ := cmd.Flags().GetString("max-size")
|
||||
maxSize, err := parseByteSize(maxSizeStr)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Invalid --max-size %q: %s\n", maxSizeStr, err)
|
||||
return
|
||||
}
|
||||
if maxSize <= 0 {
|
||||
con.PrintErrorf("--max-size must be greater than zero\n")
|
||||
return
|
||||
}
|
||||
|
||||
offset, _ := cmd.Flags().GetInt64("offset")
|
||||
if offset < 0 {
|
||||
con.PrintErrorf("--offset must be zero or positive\n")
|
||||
return
|
||||
}
|
||||
|
||||
fetchSize := maxSize + 1
|
||||
if fetchSize < maxSize {
|
||||
fetchSize = maxSize
|
||||
}
|
||||
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Downloading %s ...", remotePath), ctrl)
|
||||
download, err := downloadRemote(ctx, con, cmd, remotePath, fetchSize)
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if download.Response != nil && download.Response.Err != "" {
|
||||
con.PrintErrorf("%s\n", download.Response.Err)
|
||||
return
|
||||
}
|
||||
if !download.Exists {
|
||||
con.PrintErrorf("Remote file does not exist: %s\n", remotePath)
|
||||
return
|
||||
}
|
||||
if download.IsDir {
|
||||
con.PrintErrorf("Remote path is a directory: %s\n", download.Path)
|
||||
return
|
||||
}
|
||||
|
||||
data := download.Data
|
||||
if download.Encoder == "gzip" {
|
||||
data, err = new(encoders.Gzip).Decode(download.Data)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Decoding failed %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if int64(len(data)) > maxSize {
|
||||
con.PrintErrorf("Remote file size %s exceeds --max-size %s\n", util.ByteCountBinary(int64(len(data))), util.ByteCountBinary(maxSize))
|
||||
return
|
||||
}
|
||||
|
||||
if offset > 0 {
|
||||
if len(data) == 0 {
|
||||
con.PrintErrorf("Offset %d out of range for empty file\n", offset)
|
||||
return
|
||||
}
|
||||
if offset >= int64(len(data)) {
|
||||
con.PrintErrorf("Offset %d out of range (file size %d bytes)\n", offset, len(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
path := download.Path
|
||||
if path == "" {
|
||||
path = remotePath
|
||||
}
|
||||
|
||||
for {
|
||||
result, err := runHexEditor(data, path, int(offset))
|
||||
if err != nil {
|
||||
con.PrintErrorf("Editor error: %s\n", err)
|
||||
return
|
||||
}
|
||||
data = result.Data
|
||||
|
||||
if result.Action == actionNone {
|
||||
return
|
||||
}
|
||||
|
||||
if !result.Modified {
|
||||
if result.Action == actionSaveQuit {
|
||||
con.PrintInfof("No changes to upload for %s\n", path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch result.Action {
|
||||
case actionSaveQuit:
|
||||
uploadNow, err := confirm("Upload changes to remote file (overwrite)?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if !uploadNow {
|
||||
keepEditing, err := confirm("Continue editing?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if keepEditing {
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := uploadEditedContent(cmd, con, path, data); err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
}
|
||||
return
|
||||
case actionQuit:
|
||||
if result.Force {
|
||||
return
|
||||
}
|
||||
discard, err := confirm("Discard changes?")
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if discard {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runHexEditor(data []byte, filename string, offset int) (editorResult, error) {
|
||||
model := newEditorModel(data, filename, offset)
|
||||
program := tea.NewProgram(model, tea.WithAltScreen())
|
||||
finalModel, err := program.Run()
|
||||
if err != nil {
|
||||
return editorResult{}, err
|
||||
}
|
||||
editor, ok := finalModel.(*editorModel)
|
||||
if !ok {
|
||||
return editorResult{}, fmt.Errorf("unexpected editor state")
|
||||
}
|
||||
return editor.result(), nil
|
||||
}
|
||||
|
||||
func downloadRemote(ctx context.Context, con *console.SliverClient, cmd *cobra.Command, path string, maxBytes int64) (*sliverpb.Download, error) {
|
||||
download, err := con.Rpc.Download(ctx, &sliverpb.DownloadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
RestrictedToFile: true,
|
||||
MaxBytes: maxBytes,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if download.Response != nil && download.Response.Async {
|
||||
taskID := download.Response.TaskID
|
||||
download = &sliverpb.Download{}
|
||||
if err := waitForBeaconTaskResponse(ctx, con, taskID, download); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return download, nil
|
||||
}
|
||||
|
||||
func uploadEditedContent(cmd *cobra.Command, con *console.SliverClient, path string, data []byte) error {
|
||||
ctx, cancel := con.GrpcContext(cmd)
|
||||
defer cancel()
|
||||
|
||||
uploadData, err := new(encoders.Gzip).Encode(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrl := make(chan bool)
|
||||
con.SpinUntil(fmt.Sprintf("Uploading %s ...", path), ctrl)
|
||||
upload, err := con.Rpc.Upload(ctx, &sliverpb.UploadReq{
|
||||
Request: con.ActiveTarget.Request(cmd),
|
||||
Path: path,
|
||||
Data: uploadData,
|
||||
Encoder: "gzip",
|
||||
IsDirectory: false,
|
||||
Overwrite: true,
|
||||
})
|
||||
if err == nil && upload.Response != nil && upload.Response.Async {
|
||||
taskID := upload.Response.TaskID
|
||||
upload = &sliverpb.Upload{}
|
||||
err = waitForBeaconTaskResponse(ctx, con, taskID, upload)
|
||||
}
|
||||
ctrl <- true
|
||||
<-ctrl
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filesystem.PrintUpload(upload, con)
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForBeaconTaskResponse(ctx context.Context, con *console.SliverClient, taskID string, out proto.Message) error {
|
||||
task, err := waitForBeaconTask(ctx, con, taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state != "completed" {
|
||||
if state == "" {
|
||||
state = "unknown"
|
||||
}
|
||||
return fmt.Errorf("beacon task %s %s", task.ID, state)
|
||||
}
|
||||
if len(task.Response) == 0 {
|
||||
return fmt.Errorf("beacon task %s returned empty response", task.ID)
|
||||
}
|
||||
return proto.Unmarshal(task.Response, out)
|
||||
}
|
||||
|
||||
func waitForBeaconTask(ctx context.Context, con *console.SliverClient, taskID string) (*clientpb.BeaconTask, error) {
|
||||
ticker := time.NewTicker(beaconPollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
task, err := con.Rpc.GetBeaconTaskContent(ctx, &clientpb.BeaconTask{ID: taskID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
state := strings.ToLower(task.State)
|
||||
if state == "completed" || state == "failed" || state == "canceled" {
|
||||
return task, nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func confirm(title string) (bool, error) {
|
||||
answer := false
|
||||
if err := forms.Confirm(title, &answer); err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
func parseByteSize(input string) (int64, error) {
|
||||
value := strings.TrimSpace(input)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("size is required")
|
||||
}
|
||||
value = strings.ReplaceAll(value, " ", "")
|
||||
value = strings.ToUpper(value)
|
||||
|
||||
idx := 0
|
||||
for idx < len(value) {
|
||||
c := value[idx]
|
||||
if (c >= '0' && c <= '9') || c == '.' {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if idx == 0 {
|
||||
return 0, fmt.Errorf("invalid size %q", input)
|
||||
}
|
||||
|
||||
numStr := value[:idx]
|
||||
unitStr := strings.TrimSpace(value[idx:])
|
||||
if unitStr == "" {
|
||||
unitStr = "B"
|
||||
}
|
||||
|
||||
multiplier := int64(1)
|
||||
switch unitStr {
|
||||
case "B":
|
||||
multiplier = 1
|
||||
case "K", "KB", "KIB":
|
||||
multiplier = 1024
|
||||
case "M", "MB", "MIB":
|
||||
multiplier = 1024 * 1024
|
||||
case "G", "GB", "GIB":
|
||||
multiplier = 1024 * 1024 * 1024
|
||||
case "T", "TB", "TIB":
|
||||
multiplier = 1024 * 1024 * 1024 * 1024
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown size unit %q", unitStr)
|
||||
}
|
||||
|
||||
numVal, err := strconv.ParseFloat(numStr, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid size %q", input)
|
||||
}
|
||||
if numVal <= 0 {
|
||||
return 0, fmt.Errorf("size must be greater than zero")
|
||||
}
|
||||
|
||||
size := int64(numVal * float64(multiplier))
|
||||
if size <= 0 {
|
||||
return 0, fmt.Errorf("size is out of range")
|
||||
}
|
||||
return size, nil
|
||||
}
|
||||
@@ -150,11 +150,15 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
}
|
||||
flags.Bind("", false, chtimesCmd, func(f *pflag.FlagSet) {
|
||||
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
|
||||
f.Bool("unix", false, "interpret timestamps as Unix seconds")
|
||||
f.Bool("unix-ms", false, "interpret timestamps as Unix milliseconds")
|
||||
f.Bool("rfc3339", false, "interpret timestamps as RFC3339")
|
||||
f.Bool("rfc1123", false, "interpret timestamps as RFC1123")
|
||||
})
|
||||
carapace.Gen(chtimesCmd).PositionalCompletion(
|
||||
carapace.ActionValues().Usage("path to file to change access timestamps"),
|
||||
carapace.ActionValues().Usage("last accessed time in DateTime format, i.e. 2006-01-02 15:04:05"),
|
||||
carapace.ActionValues().Usage("last modified time in DateTime format, i.e. 2006-01-02 15:04:05"),
|
||||
carapace.ActionValues().Usage("last accessed time (default: 2006-01-02 15:04:05; see --help for format flags)"),
|
||||
carapace.ActionValues().Usage("last modified time (default: 2006-01-02 15:04:05; see --help for format flags)"),
|
||||
)
|
||||
|
||||
getprivsCmd := &cobra.Command{
|
||||
|
||||
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/tasks"
|
||||
"github.com/bishopfox/sliver/client/command/wasm"
|
||||
"github.com/bishopfox/sliver/client/command/wireguard"
|
||||
"github.com/bishopfox/sliver/client/command/hexedit"
|
||||
client "github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/reeflective/console"
|
||||
@@ -96,6 +97,7 @@ func SliverCommands(con *client.SliverClient) console.Commands {
|
||||
// [ Filesystem ]
|
||||
bind(consts.FilesystemHelpGroup,
|
||||
edit.Commands,
|
||||
hexedit.Commands,
|
||||
filesystem.Commands,
|
||||
)
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ const (
|
||||
PwdStr = "pwd"
|
||||
CatStr = "cat"
|
||||
EditStr = "edit"
|
||||
HexEditStr = "hex-edit"
|
||||
DownloadStr = "download"
|
||||
HeadStr = "head"
|
||||
TailStr = "tail"
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/leaky"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -210,6 +211,8 @@ const leakyBufSize = 4108 // data.len(2) + hmacsha1(10) + data(4096)
|
||||
var leakyBuf = leaky.NewLeakyBuf(2048, leakyBufSize)
|
||||
|
||||
func connect(conn net.Conn, stream rpcpb.SliverRPC_SocksProxyClient, frame *sliverpb.SocksData) {
|
||||
// Client Rate Limiter: 10 operations per second, burst of 1
|
||||
limiter := rate.NewLimiter(rate.Limit(10), 1)
|
||||
|
||||
SocksConnPool.Store(frame.TunnelID, conn)
|
||||
|
||||
@@ -241,6 +244,11 @@ func connect(conn net.Conn, stream rpcpb.SliverRPC_SocksProxyClient, frame *sliv
|
||||
return
|
||||
}
|
||||
if n > 0 {
|
||||
if err := limiter.Wait(context.Background()); err != nil {
|
||||
log.Printf("[socks] rate limiter error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
frame.Data = buff[:n]
|
||||
frame.Sequence = ToImplantSequence
|
||||
log.Printf("[socks] (User to Client) to Server to agent Data Sequence %d , Data Size %d \n", ToImplantSequence, len(frame.Data))
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/bishopfox/sliver/implant/sliver/extension"
|
||||
"github.com/bishopfox/sliver/implant/sliver/mount"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"google.golang.org/protobuf/proto"
|
||||
@@ -56,6 +57,7 @@ var (
|
||||
pb.MsgNetstatReq: netstatHandler,
|
||||
|
||||
pb.MsgSideloadReq: sideloadHandler,
|
||||
pb.MsgMountReq: mountHandler,
|
||||
|
||||
pb.MsgReconfigureReq: reconfigureHandler,
|
||||
pb.MsgSSHCommandReq: runSSHCommandHandler,
|
||||
@@ -154,6 +156,27 @@ func listExtensionsHandler(data []byte, resp RPCResponse) {
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func mountHandler(data []byte, resp RPCResponse) {
|
||||
mountReq := &pb.MountReq{}
|
||||
err := proto.Unmarshal(data, mountReq)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mountData, err := mount.GetMountInformation()
|
||||
mountResp := &pb.Mount{
|
||||
Info: mountData,
|
||||
Response: &commonpb.Response{},
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
mountResp.Response.Err = err.Error()
|
||||
}
|
||||
|
||||
data, err = proto.Marshal(mountResp)
|
||||
resp(data, err)
|
||||
}
|
||||
|
||||
func getUid(fileInfo os.FileInfo) string {
|
||||
uid := int32(fileInfo.Sys().(*syscall.Stat_t).Uid)
|
||||
uid_str := strconv.FormatUint(uint64(uid), 10)
|
||||
|
||||
@@ -36,16 +36,29 @@ import (
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
inactivityCheckInterval = 4 * time.Second
|
||||
inactivityTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type socksTunnelPool struct {
|
||||
tunnels *sync.Map // map[uint64]chan []byte
|
||||
tunnels *sync.Map // map[uint64]chan []byte
|
||||
lastActivity *sync.Map // map[uint64]time.Time
|
||||
}
|
||||
|
||||
var socksTunnels = socksTunnelPool{
|
||||
tunnels: &sync.Map{},
|
||||
tunnels: &sync.Map{},
|
||||
lastActivity: &sync.Map{},
|
||||
}
|
||||
|
||||
var socksServer *socks5.Server
|
||||
|
||||
// Initialize socks server
|
||||
func init() {
|
||||
socksServer = socks5.NewServer()
|
||||
socksTunnels.startCleanupMonitor()
|
||||
}
|
||||
|
||||
func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
|
||||
socksData := &sliverpb.SocksData{}
|
||||
err := proto.Unmarshal(envelope.Data, socksData)
|
||||
@@ -55,9 +68,26 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{end}}
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
// Check early to see if this is a close request from server
|
||||
if socksData.CloseConn {
|
||||
if tunnel, ok := socksTunnels.tunnels.LoadAndDelete(socksData.TunnelID); ok {
|
||||
if ch, ok := tunnel.(chan []byte); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
socksTunnels.lastActivity.Delete(socksData.TunnelID)
|
||||
return
|
||||
}
|
||||
|
||||
if socksData.Data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Record activity as soon as we get data for this tunnel
|
||||
socksTunnels.recordActivity(socksData.TunnelID)
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[socks] User to Client to (server to implant) Data Sequence %d, Data Size %d\n", socksData.Sequence, len(socksData.Data))
|
||||
// {{end}}
|
||||
@@ -70,8 +100,6 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
socksServer = socks5.NewServer(
|
||||
socks5.WithAuthMethods([]socks5.Authenticator{auth}),
|
||||
)
|
||||
} else {
|
||||
socksServer = socks5.NewServer()
|
||||
}
|
||||
|
||||
// {{if .Config.Debug}}
|
||||
@@ -80,7 +108,7 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
|
||||
// init tunnel
|
||||
if tunnel, ok := socksTunnels.tunnels.Load(socksData.TunnelID); !ok {
|
||||
tunnelChan := make(chan []byte, 10)
|
||||
tunnelChan := make(chan []byte, 100) // Buffered channel for 100 messages
|
||||
socksTunnels.tunnels.Store(socksData.TunnelID, tunnelChan)
|
||||
tunnelChan <- socksData.Data
|
||||
err := socksServer.ServeConn(&socks{stream: socksData, conn: connection})
|
||||
@@ -88,9 +116,12 @@ func SocksReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
|
||||
// {{if .Config.Debug}}
|
||||
log.Printf("[socks] Failed to serve connection: %v", err)
|
||||
// {{end}}
|
||||
// Cleanup on serve failure
|
||||
socksTunnels.tunnels.Delete(socksData.TunnelID)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Will block when channel is full
|
||||
tunnel.(chan []byte) <- socksData.Data
|
||||
}
|
||||
}
|
||||
@@ -105,16 +136,22 @@ type socks struct {
|
||||
}
|
||||
|
||||
func (s *socks) Read(b []byte) (n int, err error) {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
channel, ok := socksTunnels.tunnels.Load(s.stream.TunnelID)
|
||||
if !ok {
|
||||
return 0, errors.New("[socks] invalid tunnel id")
|
||||
}
|
||||
|
||||
socksTunnels.recordActivity(s.stream.TunnelID)
|
||||
data := <-channel.(chan []byte)
|
||||
return copy(b, data), nil
|
||||
}
|
||||
|
||||
func (s *socks) Write(b []byte) (n int, err error) {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
socksTunnels.recordActivity(s.stream.TunnelID)
|
||||
data, err := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: s.stream.TunnelID,
|
||||
Data: b,
|
||||
@@ -136,12 +173,15 @@ func (s *socks) Write(b []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
func (s *socks) Close() error {
|
||||
time.Sleep(10 * time.Millisecond) // Necessary delay
|
||||
|
||||
channel, ok := socksTunnels.tunnels.LoadAndDelete(s.stream.TunnelID)
|
||||
if !ok {
|
||||
return errors.New("[socks] can't close unknown channel")
|
||||
}
|
||||
close(channel.(chan []byte))
|
||||
|
||||
// Signal to server that we need to close this tunnel
|
||||
data, err := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: s.stream.TunnelID,
|
||||
CloseConn: true,
|
||||
@@ -181,3 +221,38 @@ func (c *socks) SetReadDeadline(t time.Time) error {
|
||||
func (c *socks) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *socksTunnelPool) recordActivity(tunnelID uint64) {
|
||||
s.lastActivity.Store(tunnelID, time.Now())
|
||||
}
|
||||
|
||||
// Periodically check for inactive tunnels and clean up
|
||||
func (s *socksTunnelPool) startCleanupMonitor() {
|
||||
go func() {
|
||||
ticker := time.NewTicker(inactivityCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
s.tunnels.Range(func(key, value interface{}) bool {
|
||||
tunnelID := key.(uint64)
|
||||
lastActivityI, exists := s.lastActivity.Load(tunnelID)
|
||||
if !exists {
|
||||
// If no activity record exists, create one
|
||||
s.recordActivity(tunnelID)
|
||||
return true
|
||||
}
|
||||
|
||||
lastActivity := lastActivityI.(time.Time)
|
||||
if time.Since(lastActivity) > inactivityTimeout {
|
||||
// Clean up the inactive tunnel
|
||||
if ch, ok := value.(chan []byte); ok {
|
||||
close(ch)
|
||||
}
|
||||
s.tunnels.Delete(tunnelID)
|
||||
s.lastActivity.Delete(tunnelID)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
//go:build darwin
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2023 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func getString(input []byte) string {
|
||||
ver := string(input)
|
||||
if i := strings.Index(ver, "\x00"); i != -1 {
|
||||
ver = ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func formatMountFlags(flags uint32) string {
|
||||
opts := make([]string, 0, 12)
|
||||
|
||||
if flags&unix.MNT_RDONLY != 0 {
|
||||
opts = append(opts, "ro")
|
||||
} else {
|
||||
opts = append(opts, "rw")
|
||||
}
|
||||
if flags&unix.MNT_NOSUID != 0 {
|
||||
opts = append(opts, "nosuid")
|
||||
}
|
||||
if flags&unix.MNT_NODEV != 0 {
|
||||
opts = append(opts, "nodev")
|
||||
}
|
||||
if flags&unix.MNT_NOEXEC != 0 {
|
||||
opts = append(opts, "noexec")
|
||||
}
|
||||
if flags&unix.MNT_SYNCHRONOUS != 0 {
|
||||
opts = append(opts, "sync")
|
||||
}
|
||||
if flags&unix.MNT_ASYNC != 0 {
|
||||
opts = append(opts, "async")
|
||||
}
|
||||
if flags&unix.MNT_NOATIME != 0 {
|
||||
opts = append(opts, "noatime")
|
||||
}
|
||||
if flags&unix.MNT_JOURNALED != 0 {
|
||||
opts = append(opts, "journaled")
|
||||
}
|
||||
if flags&unix.MNT_QUOTA != 0 {
|
||||
opts = append(opts, "quota")
|
||||
}
|
||||
if flags&unix.MNT_UNION != 0 {
|
||||
opts = append(opts, "union")
|
||||
}
|
||||
if flags&unix.MNT_AUTOMOUNTED != 0 {
|
||||
opts = append(opts, "automounted")
|
||||
}
|
||||
if flags&unix.MNT_REMOVABLE != 0 {
|
||||
opts = append(opts, "removable")
|
||||
}
|
||||
if flags&unix.MNT_DONTBROWSE != 0 {
|
||||
opts = append(opts, "nobrowse")
|
||||
}
|
||||
if flags&unix.MNT_IGNORE_OWNERSHIP != 0 {
|
||||
opts = append(opts, "ignore-ownership")
|
||||
}
|
||||
if flags&unix.MNT_LOCAL != 0 {
|
||||
opts = append(opts, "local")
|
||||
}
|
||||
if flags&unix.MNT_ROOTFS != 0 {
|
||||
opts = append(opts, "rootfs")
|
||||
}
|
||||
if flags&unix.MNT_SNAPSHOT != 0 {
|
||||
opts = append(opts, "snapshot")
|
||||
}
|
||||
if flags&unix.MNT_CPROTECT != 0 {
|
||||
opts = append(opts, "cprotect")
|
||||
}
|
||||
if flags&unix.MNT_NOUSERXATTR != 0 {
|
||||
opts = append(opts, "nouserxattr")
|
||||
}
|
||||
if flags&unix.MNT_QUARANTINE != 0 {
|
||||
opts = append(opts, "quarantine")
|
||||
}
|
||||
if flags&unix.MNT_UNKNOWNPERMISSIONS != 0 {
|
||||
opts = append(opts, "unknown-permissions")
|
||||
}
|
||||
if flags&unix.MNT_DEFWRITE != 0 {
|
||||
opts = append(opts, "defwrite")
|
||||
}
|
||||
if flags&unix.MNT_DOVOLFS != 0 {
|
||||
opts = append(opts, "volfs")
|
||||
}
|
||||
if flags&unix.MNT_NOBLOCK != 0 {
|
||||
opts = append(opts, "noblock")
|
||||
}
|
||||
if flags&unix.MNT_RELOAD != 0 {
|
||||
opts = append(opts, "reload")
|
||||
}
|
||||
if flags&unix.MNT_UPDATE != 0 {
|
||||
opts = append(opts, "update")
|
||||
}
|
||||
if flags&unix.MNT_EXPORTED != 0 {
|
||||
opts = append(opts, "exported")
|
||||
}
|
||||
if flags&unix.MNT_MULTILABEL != 0 {
|
||||
opts = append(opts, "multilabel")
|
||||
}
|
||||
if flags&unix.MNT_STRICTATIME != 0 {
|
||||
opts = append(opts, "strictatime")
|
||||
}
|
||||
if flags&unix.MNT_FORCE != 0 {
|
||||
opts = append(opts, "force")
|
||||
}
|
||||
|
||||
return strings.Join(opts, ",")
|
||||
}
|
||||
|
||||
func GetMountInformation() ([]*sliverpb.MountInfo, error) {
|
||||
mountInfo := make([]*sliverpb.MountInfo, 0)
|
||||
|
||||
count, err := unix.Getfsstat(nil, unix.MNT_NOWAIT)
|
||||
if err != nil || count == 0 {
|
||||
return mountInfo, err
|
||||
}
|
||||
|
||||
stats := make([]unix.Statfs_t, count)
|
||||
count, err = unix.Getfsstat(stats, unix.MNT_NOWAIT)
|
||||
if err != nil {
|
||||
return mountInfo, err
|
||||
}
|
||||
|
||||
stats = stats[:count]
|
||||
for _, stat := range stats {
|
||||
var mountData sliverpb.MountInfo
|
||||
mountData.VolumeName = getString(stat.Mntfromname[:])
|
||||
mountData.MountPoint = getString(stat.Mntonname[:])
|
||||
mountData.Label = "/"
|
||||
mountData.VolumeType = getString(stat.Fstypename[:])
|
||||
mountData.MountOptions = formatMountFlags(stat.Flags)
|
||||
mountData.TotalSpace = stat.Blocks * uint64(stat.Bsize)
|
||||
mountData.FreeSpace = stat.Bavail * uint64(stat.Bsize)
|
||||
mountData.UsedSpace = (stat.Blocks - stat.Bfree) * uint64(stat.Bsize)
|
||||
mountInfo = append(mountInfo, &mountData)
|
||||
}
|
||||
|
||||
return mountInfo, nil
|
||||
}
|
||||
@@ -1,7 +1,25 @@
|
||||
// +linux
|
||||
//go:build linux
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2023 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package mount
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
@@ -19,8 +20,6 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package mount
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +darwin
|
||||
//go:build darwin
|
||||
|
||||
package procdump
|
||||
|
||||
|
||||
@@ -18,6 +18,129 @@ package version
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
func GetVersion() string {
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
//{{if .Config.Debug}}
|
||||
"log"
|
||||
//{{end}}
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type plist struct {
|
||||
XMLName xml.Name `xml:"plist"`
|
||||
Dict dict `xml:"dict"`
|
||||
}
|
||||
|
||||
type dict struct {
|
||||
Key []string `xml:"key"`
|
||||
String []string `xml:"string"`
|
||||
}
|
||||
|
||||
func getString(input []byte) string {
|
||||
ver := string(input)
|
||||
if i := strings.Index(ver, "\x00"); i != -1 {
|
||||
ver = ver[:i]
|
||||
}
|
||||
return ver
|
||||
}
|
||||
|
||||
func readOSRelease() string {
|
||||
paths := []string{
|
||||
"/System/Library/CoreServices/SystemVersion.plist",
|
||||
"/System/Library/CoreServices/ServerVersion.plist",
|
||||
}
|
||||
for _, path := range paths {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
properties, err := parsePlistFile(file)
|
||||
_ = file.Close()
|
||||
if err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error parsing %s: %v", path, err)
|
||||
//{{end}}
|
||||
continue
|
||||
}
|
||||
if osRelease := buildOSRelease(properties); osRelease != "" {
|
||||
return osRelease
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parsePlistFile(file *os.File) (map[string]string, error) {
|
||||
var v plist
|
||||
if err := xml.NewDecoder(file).Decode(&v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(v.Dict.Key) != len(v.Dict.String) {
|
||||
return nil, errors.New("invalid plist content")
|
||||
}
|
||||
properties := make(map[string]string, len(v.Dict.Key))
|
||||
for i, key := range v.Dict.Key {
|
||||
properties[key] = v.Dict.String[i]
|
||||
}
|
||||
return properties, nil
|
||||
}
|
||||
|
||||
func buildOSRelease(properties map[string]string) string {
|
||||
productName := properties["ProductName"]
|
||||
productVersion := properties["ProductVersion"]
|
||||
productBuildVersion := properties["ProductBuildVersion"]
|
||||
if productName == "" || productVersion == "" || productBuildVersion == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s %s (%s)", productName, productVersion, productBuildVersion)
|
||||
}
|
||||
|
||||
func normalizeArch(arch string) string {
|
||||
arch = strings.TrimSpace(arch)
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(arch) {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "i386", "i486", "i586", "i686", "x86":
|
||||
return "x86"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
|
||||
func formatDetailedVersion(osRelease, kernel, arch string) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if osRelease != "" {
|
||||
parts = append(parts, osRelease)
|
||||
}
|
||||
if kernel != "" {
|
||||
parts = append(parts, fmt.Sprintf("kernel %s", kernel))
|
||||
}
|
||||
if arch != "" {
|
||||
parts = append(parts, arch)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func GetVersion() string {
|
||||
osRelease := readOSRelease()
|
||||
var uname unix.Utsname
|
||||
if err := unix.Uname(&uname); err != nil {
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error getting OS version: %v", err)
|
||||
//{{end}}
|
||||
return osRelease
|
||||
}
|
||||
if osRelease == "" {
|
||||
osRelease = getString(uname.Sysname[:])
|
||||
}
|
||||
kernel := getString(uname.Release[:])
|
||||
arch := normalizeArch(getString(uname.Machine[:]))
|
||||
return formatDetailedVersion(osRelease, kernel, arch)
|
||||
}
|
||||
@@ -22,7 +22,10 @@ package version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
//{{if .Config.Debug}}
|
||||
"log"
|
||||
//{{end}}
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
@@ -39,11 +42,111 @@ func getString(input [65]int8) string {
|
||||
return ver
|
||||
}
|
||||
|
||||
func readOSRelease() string {
|
||||
paths := []string{"/etc/os-release", "/usr/lib/os-release"}
|
||||
for _, path := range paths {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return buildOSRelease(parseOSRelease(string(data)))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func parseOSRelease(content string) map[string]string {
|
||||
values := make(map[string]string)
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
key, value, ok := strings.Cut(line, "=")
|
||||
if !ok || key == "" {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimSpace(key)
|
||||
value = unescape(unquote(strings.TrimSpace(value)))
|
||||
values[key] = value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func unquote(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
}
|
||||
if (s[0] == '"' || s[0] == '\'') && s[0] == s[len(s)-1] {
|
||||
return s[1 : len(s)-1]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func unescape(s string) string {
|
||||
return strings.NewReplacer(
|
||||
`\$`, `$`,
|
||||
`\"`, `"`,
|
||||
`\'`, `'`,
|
||||
`\\`, `\`,
|
||||
"\\`", "`",
|
||||
).Replace(s)
|
||||
}
|
||||
|
||||
func buildOSRelease(values map[string]string) string {
|
||||
name := values["NAME"]
|
||||
version := values["VERSION"]
|
||||
if version == "" {
|
||||
version = values["VERSION_ID"]
|
||||
}
|
||||
if name != "" && version != "" {
|
||||
return fmt.Sprintf("%s %s", name, version)
|
||||
}
|
||||
return values["PRETTY_NAME"]
|
||||
}
|
||||
|
||||
func normalizeArch(arch string) string {
|
||||
arch = strings.TrimSpace(arch)
|
||||
if arch == "" {
|
||||
return ""
|
||||
}
|
||||
switch strings.ToLower(arch) {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "i386", "i486", "i586", "i686", "x86":
|
||||
return "x86"
|
||||
default:
|
||||
return arch
|
||||
}
|
||||
}
|
||||
|
||||
func formatDetailedVersion(osRelease, kernel, arch string) string {
|
||||
parts := make([]string, 0, 3)
|
||||
if osRelease != "" {
|
||||
parts = append(parts, osRelease)
|
||||
}
|
||||
if kernel != "" {
|
||||
parts = append(parts, fmt.Sprintf("kernel %s", kernel))
|
||||
}
|
||||
if arch != "" {
|
||||
parts = append(parts, arch)
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// GetVersion returns the os version information
|
||||
func GetVersion() string {
|
||||
osRelease := readOSRelease()
|
||||
var uname syscall.Utsname
|
||||
if err := syscall.Uname(&uname); err != nil {
|
||||
log.Fatal(err)
|
||||
//{{if .Config.Debug}}
|
||||
log.Printf("error getting OS version: %v", err)
|
||||
//{{end}}
|
||||
return osRelease
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", getString(uname.Sysname), getString(uname.Nodename), getString(uname.Release))
|
||||
if osRelease == "" {
|
||||
osRelease = getString(uname.Sysname)
|
||||
}
|
||||
kernel := getString(uname.Release)
|
||||
arch := normalizeArch(getString(uname.Machine))
|
||||
return formatDetailedVersion(osRelease, kernel, arch)
|
||||
}
|
||||
+322
-50
@@ -20,8 +20,11 @@ package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
@@ -32,15 +35,16 @@ import (
|
||||
|
||||
var (
|
||||
// SessionID->Tunnels[TunnelID]->Tunnel->Cache map[uint64]*sliverpb.SocksData{}
|
||||
toImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}}
|
||||
toImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}, lastActivity: map[uint64]time.Time{}}
|
||||
|
||||
// SessionID->Tunnels[TunnelID]->Tunnel->Cache
|
||||
fromImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}}
|
||||
fromImplantCacheSocks = socksDataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.SocksData{}, lastActivity: map[uint64]time.Time{}}
|
||||
)
|
||||
|
||||
type socksDataCache struct {
|
||||
mutex *sync.RWMutex
|
||||
cache map[uint64]map[uint64]*sliverpb.SocksData
|
||||
mutex *sync.RWMutex
|
||||
cache map[uint64]map[uint64]*sliverpb.SocksData
|
||||
lastActivity map[uint64]time.Time
|
||||
}
|
||||
|
||||
func (c *socksDataCache) Add(tunnelID uint64, sequence uint64, tunnelData *sliverpb.SocksData) {
|
||||
@@ -72,6 +76,7 @@ func (c *socksDataCache) DeleteTun(tunnelID uint64) {
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
delete(c.cache, tunnelID)
|
||||
delete(c.lastActivity, tunnelID)
|
||||
}
|
||||
|
||||
func (c *socksDataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
|
||||
@@ -85,69 +90,299 @@ func (c *socksDataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
|
||||
delete(c.cache[tunnelID], sequence)
|
||||
}
|
||||
|
||||
func (c *socksDataCache) recordActivity(tunnelID uint64) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.lastActivity[tunnelID] = time.Now()
|
||||
}
|
||||
|
||||
// Socks - Open an in-band port forward
|
||||
|
||||
const (
|
||||
writeTimeout = 5 * time.Second
|
||||
batchSize = 100 // Maximum number of sequences to batch
|
||||
inactivityCheckInterval = 4 * time.Second
|
||||
inactivityTimeout = 15 * time.Second
|
||||
ToImplantTickerInterval = 10 * time.Millisecond // data going towards implant is usually smaller request data
|
||||
ToClientTickerInterval = 5 * time.Millisecond // data coming back from implant is usually larger response data
|
||||
)
|
||||
|
||||
func (s *Server) SocksProxy(stream rpcpb.SliverRPC_SocksProxyServer) error {
|
||||
errChan := make(chan error, 2)
|
||||
defer close(errChan)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
connDone := make(chan struct{})
|
||||
defer close(connDone)
|
||||
|
||||
// Track all goroutines spawned for this session
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
// Track all tunnels created for this session
|
||||
activeTunnels := make(map[uint64]bool)
|
||||
var tunnelMutex sync.Mutex
|
||||
|
||||
// Cleanup all tunnels on SocksProxy closure
|
||||
defer func() {
|
||||
tunnelMutex.Lock()
|
||||
for tunnelID := range activeTunnels {
|
||||
if tunnel := core.SocksTunnels.Get(tunnelID); tunnel != nil {
|
||||
rpcLog.Infof("Cleaning up tunnel %d on proxy closure", tunnelID)
|
||||
close(tunnel.FromImplant)
|
||||
tunnel.Client = nil
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
}
|
||||
}
|
||||
tunnelMutex.Unlock()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-errChan:
|
||||
rpcLog.Errorf("SocksProxy error: %v", err)
|
||||
return err
|
||||
default:
|
||||
}
|
||||
|
||||
fromClient, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
return nil
|
||||
}
|
||||
//fmt.Println("Send Agent 1 ",fromClient.TunnelID,len(fromClient.Data))
|
||||
if err != nil {
|
||||
rpcLog.Warnf("Error on stream recv %s", err)
|
||||
return rpcError(err)
|
||||
}
|
||||
tunnelLog.Debugf("Tunnel %d: From client %d byte(s)",
|
||||
fromClient.TunnelID, len(fromClient.Data))
|
||||
socks := core.SocksTunnels.Get(fromClient.TunnelID)
|
||||
if socks == nil {
|
||||
return nil
|
||||
|
||||
tunnelMutex.Lock()
|
||||
activeTunnels[fromClient.TunnelID] = true // Mark this as an active tunnel
|
||||
tunnelMutex.Unlock()
|
||||
|
||||
tunnel := core.SocksTunnels.Get(fromClient.TunnelID)
|
||||
if tunnel == nil {
|
||||
continue
|
||||
}
|
||||
if socks.Client == nil {
|
||||
socks.Client = stream // Bind client to tunnel
|
||||
// Send Client
|
||||
go func() {
|
||||
for tunnelData := range socks.FromImplant {
|
||||
|
||||
fromImplantCacheSocks.Add(fromClient.TunnelID, tunnelData.Sequence, tunnelData)
|
||||
if tunnel.Client == nil {
|
||||
tunnel.Client = stream
|
||||
tunnel.FromImplant = make(chan *sliverpb.SocksData, 100) // Buffered channel for 100 messages
|
||||
|
||||
for recv, ok := fromImplantCacheSocks.Get(fromClient.TunnelID, socks.FromImplantSequence); ok; recv, ok = fromImplantCacheSocks.Get(fromClient.TunnelID, socks.FromImplantSequence) {
|
||||
rpcLog.Debugf("[socks] agent to (Server To Client) Data Sequence %d , Data Size %d ,Data %v\n", socks.FromImplantSequence, len(recv.Data), recv.Data)
|
||||
socks.Client.Send(&sliverpb.SocksData{
|
||||
CloseConn: recv.CloseConn,
|
||||
TunnelID: recv.TunnelID,
|
||||
Data: recv.Data,
|
||||
})
|
||||
|
||||
fromImplantCacheSocks.DeleteSeq(fromClient.TunnelID, socks.FromImplantSequence)
|
||||
socks.FromImplantSequence++
|
||||
// Monitor tunnel goroutines for inactivity and cleanup
|
||||
wg.Add(1)
|
||||
go func(tunnelID uint64) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in monitor: %v", r)
|
||||
errChan <- fmt.Errorf("monitor goroutine panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(inactivityCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
tunnel := core.SocksTunnels.Get(tunnelID)
|
||||
if tunnel == nil || tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
session := core.Sessions.Get(tunnel.SessionID)
|
||||
|
||||
// Check both caches for activity
|
||||
toImplantCacheSocks.mutex.RLock()
|
||||
fromImplantCacheSocks.mutex.RLock()
|
||||
toLastActivity := toImplantCacheSocks.lastActivity[tunnelID]
|
||||
fromLastActivity := fromImplantCacheSocks.lastActivity[tunnelID]
|
||||
toImplantCacheSocks.mutex.RUnlock()
|
||||
fromImplantCacheSocks.mutex.RUnlock()
|
||||
|
||||
// Clean up goroutine if both directions have hit the idle threshold
|
||||
if time.Since(toLastActivity) > inactivityTimeout && time.Since(fromLastActivity) > inactivityTimeout {
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up goroutine if the client has disconnected early
|
||||
if tunnel.Client == nil || session == nil {
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: tunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}(fromClient.TunnelID)
|
||||
|
||||
// Send Client
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in client sender: %v", r)
|
||||
errChan <- fmt.Errorf("client sender panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
pendingData := make(map[uint64]*sliverpb.SocksData)
|
||||
ticker := time.NewTicker(ToClientTickerInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case tunnelData, ok := <-tunnel.FromImplant:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if implant is requesting to close the tunnel
|
||||
if tunnelData.CloseConn {
|
||||
// Clean up the tunnel
|
||||
s.CloseSocks(context.Background(), &sliverpb.Socks{TunnelID: fromClient.TunnelID})
|
||||
return
|
||||
}
|
||||
|
||||
sequence := tunnelData.Sequence
|
||||
fromImplantCacheSocks.Add(fromClient.TunnelID, sequence, tunnelData)
|
||||
pendingData[sequence] = tunnelData
|
||||
fromImplantCacheSocks.recordActivity(fromClient.TunnelID)
|
||||
|
||||
case <-ticker.C:
|
||||
if tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
if len(pendingData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
expectedSequence := atomic.LoadUint64(&tunnel.FromImplantSequence)
|
||||
processed := 0
|
||||
|
||||
// Perform Batching
|
||||
for i := 0; i < batchSize && processed < len(pendingData); i++ {
|
||||
data, exists := pendingData[expectedSequence]
|
||||
if !exists {
|
||||
break // Stop batching if we don't have the next expected sequence
|
||||
}
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errChan <- fmt.Errorf("Client sender panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := stream.Send(&sliverpb.SocksData{
|
||||
CloseConn: data.CloseConn,
|
||||
TunnelID: data.TunnelID,
|
||||
Data: data.Data,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Send error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
delete(pendingData, expectedSequence)
|
||||
fromImplantCacheSocks.DeleteSeq(fromClient.TunnelID, expectedSequence)
|
||||
atomic.AddUint64(&tunnel.FromImplantSequence, 1)
|
||||
expectedSequence++
|
||||
processed++
|
||||
}()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Send Agent
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in agent sender: %v", r)
|
||||
errChan <- fmt.Errorf("agent sender panic: %v", r)
|
||||
cancel() // Cancel context in case of a panic
|
||||
}
|
||||
}()
|
||||
|
||||
pendingData := make(map[uint64]*sliverpb.SocksData)
|
||||
ticker := time.NewTicker(ToImplantTickerInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-connDone:
|
||||
return
|
||||
case <-ticker.C:
|
||||
if tunnel.Client == nil {
|
||||
return
|
||||
}
|
||||
sequence := atomic.LoadUint64(&tunnel.ToImplantSequence)
|
||||
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from processing panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
recv, ok := toImplantCacheSocks.Get(fromClient.TunnelID, sequence)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
session := core.Sessions.Get(fromClient.Request.SessionID)
|
||||
if session == nil {
|
||||
rpcLog.Error("Session not found")
|
||||
break
|
||||
}
|
||||
|
||||
data, err := proto.Marshal(recv)
|
||||
if err != nil {
|
||||
rpcLog.Errorf("Failed to marshal data: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}:
|
||||
toImplantCacheSocks.DeleteSeq(fromClient.TunnelID, sequence)
|
||||
atomic.AddUint64(&tunnel.ToImplantSequence, 1)
|
||||
sequence++
|
||||
case <-time.After(writeTimeout):
|
||||
rpcLog.Error("Write timeout to implant")
|
||||
pendingData[sequence] = recv
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Send Agent
|
||||
go func() {
|
||||
toImplantCacheSocks.Add(fromClient.TunnelID, fromClient.Sequence, fromClient)
|
||||
|
||||
for recv, ok := toImplantCacheSocks.Get(fromClient.TunnelID, socks.ToImplantSequence); ok; recv, ok = toImplantCacheSocks.Get(fromClient.TunnelID, socks.ToImplantSequence) {
|
||||
rpcLog.Debugf("[socks] Client to (Server To Agent) Data Sequence %d , Data Size %d \n", socks.ToImplantSequence, len(fromClient.Data))
|
||||
data, _ := proto.Marshal(recv)
|
||||
|
||||
session := core.Sessions.Get(fromClient.Request.SessionID)
|
||||
session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}
|
||||
|
||||
toImplantCacheSocks.DeleteSeq(fromClient.TunnelID, socks.ToImplantSequence)
|
||||
socks.ToImplantSequence++
|
||||
}
|
||||
|
||||
}()
|
||||
toImplantCacheSocks.Add(fromClient.TunnelID, fromClient.Sequence, fromClient)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSocks5 - Create requests we close a Socks
|
||||
@@ -169,11 +404,48 @@ func (s *Server) CreateSocks(ctx context.Context, req *sliverpb.Socks) (*sliverp
|
||||
|
||||
// CloseSocks - Client requests we close a Socks
|
||||
func (s *Server) CloseSocks(ctx context.Context, req *sliverpb.Socks) (*commonpb.Empty, error) {
|
||||
err := core.SocksTunnels.Close(req.TunnelID)
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
rpcLog.Errorf("Recovered from panic in CloseSocks for tunnel %d: %v", req.TunnelID, r)
|
||||
}
|
||||
}()
|
||||
|
||||
tunnel := core.SocksTunnels.Get(req.TunnelID)
|
||||
if tunnel != nil {
|
||||
// Signal close to implant first
|
||||
if session := core.Sessions.Get(tunnel.SessionID); session != nil {
|
||||
data, _ := proto.Marshal(&sliverpb.SocksData{
|
||||
TunnelID: req.TunnelID,
|
||||
CloseConn: true,
|
||||
})
|
||||
session.Connection.Send <- &sliverpb.Envelope{
|
||||
Type: sliverpb.MsgSocksData,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond) // Delay to allow close message to be sent
|
||||
tunnel.Client = nil // Cleanup the tunnel
|
||||
if tunnel.FromImplant != nil {
|
||||
select {
|
||||
case _, ok := <-tunnel.FromImplant:
|
||||
if ok {
|
||||
close(tunnel.FromImplant)
|
||||
}
|
||||
default:
|
||||
close(tunnel.FromImplant)
|
||||
}
|
||||
tunnel.FromImplant = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up caches
|
||||
toImplantCacheSocks.DeleteTun(req.TunnelID)
|
||||
fromImplantCacheSocks.DeleteTun(req.TunnelID)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
|
||||
// Remove from core tunnels last
|
||||
if err := core.SocksTunnels.Close(req.TunnelID); err != nil {
|
||||
rpcLog.Errorf("Error closing tunnel %d: %v", req.TunnelID, err)
|
||||
}
|
||||
|
||||
return &commonpb.Empty{}, nil
|
||||
}
|
||||
Reference in new issue
Block a user