major refactoring. bug fixes and changede the file structures for readablility etc.
@@ -1,3 +0,0 @@
|
||||
#include <Havoc/DBManager/DBManager.hpp>
|
||||
|
||||
using namespace HavocNamespace;
|
||||
@@ -1,13 +1,13 @@
|
||||
<div align="center">
|
||||
<img width="125px" src="Assets/Havoc.png" />
|
||||
<img width="125px" src="assets/Havoc.png" />
|
||||
<h1>Havoc</h1>
|
||||
<br/>
|
||||
|
||||
<p><i>Havoc is a modern and malleable post-exploitation command and control framework, created by <a href="https://twitter.com/C5pider">@C5pider</a>.</i></p>
|
||||
<br />
|
||||
|
||||
<img src="Assets/Screenshots/FullSessionGraph.jpeg" width="90%" /><br />
|
||||
<img src="Assets/Screenshots/MultiUserAgentControl.png" width="90%" /><br />
|
||||
<img src="assets/Screenshots/FullSessionGraph.jpeg" width="90%" /><br />
|
||||
<img src="assets/Screenshots/MultiUserAgentControl.png" width="90%" /><br />
|
||||
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@ See the [Installation](https://github.com/HavocFramework/Havoc/wiki#installation
|
||||
- Variety of built-in post-exploitation commands
|
||||
|
||||
<div align="center">
|
||||
<img src="Assets/Screenshots/SessionConsoleHelp.png" width="90%" /><br />
|
||||
<img src="assets/Screenshots/SessionConsoleHelp.png" width="90%" /><br />
|
||||
</div>
|
||||
|
||||
#### Extensibility
|
||||
@@ -78,3 +78,9 @@ You can join the official [Havoc Discord](https://discord.gg/z3PF3NRDE5) to chat
|
||||
### Contributing
|
||||
|
||||
To contribute to the Havoc Framework, please review the guidelines in [Contributing.md](https://github.com/HavocFramework/Havoc/blob/main/CONTRIBUTING.MD) and then open a pull-request!
|
||||
|
||||
### Note
|
||||
|
||||
Please do not open any issues regarding detection.
|
||||
|
||||
The Havoc Framework hasn't been developed to be evasive. Rather it has been designed to be as malleable & modular as possible. Giving the operator the capability to add custom features or modules that evades their targets detection system.
|
||||
@@ -1,48 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"Havoc/pkg/colors"
|
||||
"Havoc/pkg/teamserver"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
TeamserverCli = &cobra.Command{
|
||||
Use: "teamserver",
|
||||
Short: "Havoc Teamserver",
|
||||
SilenceUsage: true,
|
||||
RunE: teamserverFunc,
|
||||
}
|
||||
|
||||
flags teamserver.TeamserverFlags
|
||||
)
|
||||
|
||||
func init() {
|
||||
TeamserverCli.CompletionOptions.DisableDefaultCmd = true
|
||||
TeamserverCli.AddCommand(ServerCli)
|
||||
}
|
||||
|
||||
func teamserverFunc(cmd *cobra.Command, args []string) error {
|
||||
startMenu()
|
||||
|
||||
if len(os.Args) <= 2 {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startMenu() {
|
||||
fmt.Println(colors.Red(" _______ _______ _______ \n │\\ /│( ___ )│\\ /│( ___ )( ____ \\\n │ ) ( ││ ( ) ││ ) ( ││ ( ) ││ ( \\/\n │ (___) ││ (___) ││ │ │ ││ │ │ ││ │ \n │ ___ ││ ___ │( ( ) )│ │ │ ││ │ \n │ ( ) ││ ( ) │ \\ \\_/ / │ │ │ ││ │ \n │ ) ( ││ ) ( │ \\ / │ (___) ││ (____/\\\n │/ \\││/ \\│ \\_/ (_______)(_______/"))
|
||||
fmt.Println()
|
||||
fmt.Println(" ", colors.Red("pwn"), "and", colors.Blue("elevate"), "until it's done")
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"Havoc/pkg/colors"
|
||||
"Havoc/pkg/events"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"Havoc/pkg/logger"
|
||||
"Havoc/pkg/logr"
|
||||
"Havoc/pkg/teamserver"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
ServerCli = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "server command",
|
||||
SilenceUsage: true,
|
||||
RunE: serverFunc,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
ServerCli.Flags().SortFlags = false
|
||||
|
||||
ServerCli.Flags().StringVarP(&flags.Server.Profile, "profile", "", "", "set havoc teamserver profile")
|
||||
|
||||
ServerCli.Flags().BoolVarP(&flags.Server.Debug, "debug", "", false, "enable debug mode")
|
||||
ServerCli.Flags().BoolVarP(&flags.Server.DebugDev, "debug-dev", "", false, "enable debug mode for developers (compiles the agent with the debug mode/macro enabled)")
|
||||
ServerCli.Flags().BoolVarP(&flags.Server.Default, "default", "d", false, "uses default profile (overwrites --profile)")
|
||||
ServerCli.Flags().BoolVarP(&flags.Server.Verbose, "verbose", "v", false, "verbose messages")
|
||||
}
|
||||
|
||||
func serverFunc(cmd *cobra.Command, args []string) error {
|
||||
var (
|
||||
DirPath, _ = os.Getwd()
|
||||
ServerTimer = time.Now()
|
||||
LogrPath = "data/loot/" + ServerTimer.Format("02.01.2006_15:04:05")
|
||||
Server *teamserver.Teamserver
|
||||
)
|
||||
|
||||
if len(os.Args) <= 2 {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
Server = teamserver.NewTeamserver()
|
||||
Server.SetServerFlags(flags)
|
||||
|
||||
logr.LogrInstance = logr.NewLogr(DirPath, LogrPath)
|
||||
logr.LogrInstance.LogrSendText = func(text string) {
|
||||
var pk = events.Teamserver.Logger(text)
|
||||
|
||||
Server.EventAppend(pk)
|
||||
Server.EventBroadcast("", pk)
|
||||
}
|
||||
|
||||
logr.LogrInstance.ServerStdOutInit()
|
||||
|
||||
startMenu()
|
||||
|
||||
if flags.Server.Debug {
|
||||
logger.SetDebug(true)
|
||||
logger.Debug("Debug mode enabled")
|
||||
}
|
||||
|
||||
logger.ShowTime(flags.Server.Verbose)
|
||||
|
||||
logger.Info(fmt.Sprintf("Havoc Framework [Version: %v] [CodeName: %v]", VersionNumber, VersionName))
|
||||
|
||||
if flags.Server.Default {
|
||||
Server.SetProfile(DirPath + "/data/havoc.yaotl")
|
||||
} else if flags.Server.Profile != "" {
|
||||
Server.SetProfile(flags.Server.Profile)
|
||||
} else {
|
||||
logger.Error("No profile specified. Specify a profile with --profile or choose the standard profile with --default")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !Server.FindSystemPackages() {
|
||||
logger.Error("Please install needed packages. Refer to the Wiki for more help.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("Time: " + colors.Yellow(ServerTimer.Format("02/01/2006 15:04:05")))
|
||||
logger.Info("Teamserver logs saved under: " + colors.Blue(LogrPath))
|
||||
|
||||
Server.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package cmd
|
||||
|
||||
var (
|
||||
VersionNumber = "0.4.1"
|
||||
VersionName = "The Fool"
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
package main
|
||||
|
||||
import "Havoc/cmd"
|
||||
import "Havoc/pkg/logger"
|
||||
|
||||
func main() {
|
||||
err := cmd.TeamserverCli.Execute()
|
||||
if err != nil {
|
||||
logger.Error("Failed to execute teamserver client")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
GOCMD = go
|
||||
GOBUILD = GO111MODULE="on" $(GOCMD) build -ldflags="-s -w -X cmd.VersionCommit=$(git rev-parse HEAD)" -o ./teamserver main.go
|
||||
|
||||
ifndef VERBOSE
|
||||
.SILENT:
|
||||
endif
|
||||
|
||||
all:
|
||||
$(GOBUILD)
|
||||
|
||||
clean:
|
||||
rm -rf ./bin
|
||||
rm -rf ./data/loot
|
||||
rm -rf ./data/x86_64-w64-mingw32-cross
|
||||
rm -rf ./data/havoc.db
|
||||
rm -rf .idea
|
||||
rm -rf ./teamserver
|
||||
@@ -1,23 +0,0 @@
|
||||
package colors
|
||||
|
||||
import "github.com/fatih/color"
|
||||
|
||||
var (
|
||||
Blue = color.New(color.FgBlue).SprintFunc()
|
||||
Red = color.New(color.FgRed).SprintFunc()
|
||||
Green = color.New(color.FgGreen).SprintFunc()
|
||||
Yellow = color.New(color.FgYellow).SprintFunc()
|
||||
White = color.New(color.FgWhite).SprintFunc()
|
||||
|
||||
BoldBlue = color.New(color.FgBlue, color.Bold).SprintFunc()
|
||||
BoldRed = color.New(color.FgRed, color.Bold).SprintFunc()
|
||||
BoldGreen = color.New(color.FgGreen, color.Bold).SprintFunc()
|
||||
BoldYellow = color.New(color.FgYellow, color.Bold).SprintFunc()
|
||||
BoldWhite = color.New(color.FgWhite, color.Bold).SprintFunc()
|
||||
|
||||
RedUnderline = color.New(color.FgRed).Add(color.Underline).SprintFunc()
|
||||
BlueUnderline = color.New(color.FgBlue).Add(color.Underline).SprintFunc()
|
||||
GreenUnderline = color.New(color.FgGreen).Add(color.Underline).SprintFunc()
|
||||
YellowUnderline = color.New(color.FgYellow).Add(color.Underline).SprintFunc()
|
||||
WhiteUnderline = color.New(color.FgWhite).Add(color.Underline).SprintFunc()
|
||||
)
|
||||
@@ -1,149 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"Havoc/pkg/agent"
|
||||
"Havoc/pkg/handlers"
|
||||
"Havoc/pkg/logr"
|
||||
"Havoc/pkg/packager"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
)
|
||||
|
||||
/* TODO: rename everything here from 'Demon' to 'Agent' */
|
||||
|
||||
var Demons demons
|
||||
|
||||
func (demons) NewDemon(DemonAgent *agent.Agent) packager.Package {
|
||||
var (
|
||||
Package packager.Package
|
||||
TempParent *agent.Agent
|
||||
TempMagic string
|
||||
)
|
||||
|
||||
Package.Head.Event = packager.Type.Session.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.Session.NewSession
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
|
||||
TempParent = DemonAgent.Pivots.Parent
|
||||
DemonAgent.Pivots.Parent = nil
|
||||
|
||||
var InfoMap = structs.Map(DemonAgent)
|
||||
var SessionInfo = InfoMap["Info"].(map[string]interface{})
|
||||
|
||||
switch DemonAgent.Info.Listener.(type) {
|
||||
case *handlers.HTTP:
|
||||
SessionInfo["Listener"] = DemonAgent.Info.Listener.(*handlers.HTTP).Config.Name
|
||||
break
|
||||
|
||||
case *handlers.SMB:
|
||||
SessionInfo["Listener"] = DemonAgent.Info.Listener.(*handlers.SMB).Config.Name
|
||||
break
|
||||
|
||||
case *handlers.External:
|
||||
SessionInfo["Listener"] = DemonAgent.Info.Listener.(*handlers.External).Config.Name
|
||||
break
|
||||
}
|
||||
|
||||
if InfoMap["Active"].(bool) {
|
||||
InfoMap["Active"] = "true"
|
||||
} else {
|
||||
InfoMap["Active"] = "false"
|
||||
}
|
||||
|
||||
delete(InfoMap, "Connection")
|
||||
delete(InfoMap, "SessionDir")
|
||||
delete(InfoMap, "Info")
|
||||
delete(InfoMap, "JobQueue")
|
||||
delete(InfoMap, "Parent")
|
||||
|
||||
TempMagic = fmt.Sprintf("%x", DemonAgent.Info.MagicValue)
|
||||
|
||||
if TempParent != nil {
|
||||
InfoMap["PivotParent"] = TempParent.NameID
|
||||
}
|
||||
|
||||
for k, v := range SessionInfo {
|
||||
switch v.(type) {
|
||||
case string:
|
||||
InfoMap[k] = v
|
||||
case bool:
|
||||
if v.(bool) {
|
||||
InfoMap[k] = "true"
|
||||
} else {
|
||||
InfoMap[k] = "false"
|
||||
}
|
||||
case int:
|
||||
InfoMap[k] = strconv.Itoa(v.(int))
|
||||
case nil:
|
||||
InfoMap[k] = "null"
|
||||
}
|
||||
}
|
||||
|
||||
InfoMap["MagicValue"] = TempMagic
|
||||
|
||||
Package.Body.Info = InfoMap
|
||||
DemonAgent.Pivots.Parent = TempParent
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func (demons) DemonOutput(DemonID string, CommandID int, Output string) packager.Package {
|
||||
var Package packager.Package
|
||||
var LogrOut map[string]string
|
||||
|
||||
err := json.Unmarshal([]byte(Output), &LogrOut)
|
||||
if err == nil && CommandID != agent.COMMAND_NOJOB {
|
||||
logr.LogrInstance.DemonAddOutput(DemonID, LogrOut, time.Now().UTC().Format("02/01/2006 15:04:05"))
|
||||
}
|
||||
|
||||
Package.Head.Event = packager.Type.Session.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.Session.Output
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
|
||||
Package.Body.Info["DemonID"] = DemonID
|
||||
Package.Body.Info["CommandID"] = strconv.Itoa(CommandID)
|
||||
Package.Body.Info["Output"] = base64.StdEncoding.EncodeToString([]byte(Output))
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func (demons) CallBack(DemonID string, callback string) packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.Session.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.Session.Output
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
|
||||
Package.Body.Info["DemonID"] = DemonID
|
||||
Package.Body.Info["CommandID"] = "10"
|
||||
Package.Body.Info["Output"] = callback
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func (demons) MarkAs(AgentID, Mark string) packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.Session.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.Session.MarkAsDead
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
|
||||
Package.Body.Info["AgentID"] = AgentID
|
||||
Package.Body.Info["Marked"] = Mark
|
||||
|
||||
return Package
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"Havoc/pkg/packager"
|
||||
"Havoc/pkg/profile"
|
||||
)
|
||||
|
||||
type (
|
||||
chatLog int
|
||||
listeners int
|
||||
demons int
|
||||
gate int
|
||||
service int
|
||||
teamserver int
|
||||
)
|
||||
|
||||
type EventInterface interface {
|
||||
EventAppend(pk packager.Package) []packager.Package
|
||||
EventBroadcast(FromUser string, pk packager.Package)
|
||||
SendEvent(id string, pk packager.Package) error
|
||||
}
|
||||
|
||||
func Authenticated(authed bool) packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.InitConnection.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
if authed {
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Success
|
||||
Package.Body.Info["Message"] = "Successful Authenticated"
|
||||
} else {
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Error
|
||||
Package.Body.Info["Message"] = "Wrong Password"
|
||||
}
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func UserAlreadyExits() packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.InitConnection.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Error
|
||||
Package.Body.Info["Message"] = "User already exits"
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func UserDoNotExists() packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.InitConnection.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Error
|
||||
Package.Body.Info["Message"] = "User doesn't exits"
|
||||
|
||||
return Package
|
||||
}
|
||||
|
||||
func SendProfile(profile *profile.Profile) packager.Package {
|
||||
var (
|
||||
Package packager.Package
|
||||
JsonBytes []byte
|
||||
Addresses string
|
||||
)
|
||||
|
||||
Package.Head.Event = packager.Type.InitConnection.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Profile
|
||||
Package.Body.Info = make(map[string]interface{})
|
||||
|
||||
JsonBytes, err := json.Marshal(*profile.Config.Demon)
|
||||
if err != nil {
|
||||
return packager.Package{}
|
||||
}
|
||||
Package.Body.Info["Demon"] = string(JsonBytes)
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return packager.Package{}
|
||||
}
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
if len(Addresses) == 0 {
|
||||
Addresses = ipnet.IP.String()
|
||||
} else {
|
||||
Addresses += ", " + ipnet.IP.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Package.Body.Info["TeamserverIPs"] = Addresses
|
||||
|
||||
return Package
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"Havoc/pkg/packager"
|
||||
)
|
||||
|
||||
var Service service
|
||||
|
||||
func (service) AgentRegister(AgentData string) packager.Package {
|
||||
var Package packager.Package
|
||||
|
||||
Package.Head.Event = packager.Type.Service.Type
|
||||
Package.Head.Time = time.Now().Format("02/01/2006 15:04:05")
|
||||
|
||||
Package.Body.SubEvent = packager.Type.InitConnection.Success
|
||||
Package.Body.Info = map[string]interface{}{
|
||||
"Agent": AgentData,
|
||||
}
|
||||
|
||||
return Package
|
||||
}
|
||||
@@ -1,465 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"Havoc/pkg/agent"
|
||||
"Havoc/pkg/common"
|
||||
"Havoc/pkg/events"
|
||||
"Havoc/pkg/logger"
|
||||
"Havoc/pkg/logr"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
/*
|
||||
Service API Module
|
||||
Interact with external services (Custom Agents, ExternalC2s etc.)
|
||||
*/
|
||||
|
||||
func NewService(engine *gin.Engine) *Service {
|
||||
var service = new(Service)
|
||||
|
||||
service.engine = engine
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) Start() {
|
||||
|
||||
s.engine.GET("/"+s.Config.Endpoint, func(context *gin.Context) {
|
||||
upgrade := websocket.Upgrader{}
|
||||
WebSocket, err := upgrade.Upgrade(context.Writer, context.Request, nil)
|
||||
if err != nil {
|
||||
logger.Error("Failed upgrading request")
|
||||
return
|
||||
}
|
||||
|
||||
go s.handleConnection(WebSocket)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) handleConnection(socket *websocket.Conn) {
|
||||
var client = new(ClientService)
|
||||
client.Conn = socket
|
||||
|
||||
if !s.authenticate(client) {
|
||||
logger.Error("Failed to authenticate service client")
|
||||
|
||||
err := client.Conn.Close()
|
||||
if err != nil {
|
||||
logger.Error("Failed to close websocket service client: " + err.Error())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
s.clients = append(s.clients, client)
|
||||
|
||||
s.routine(client)
|
||||
}
|
||||
|
||||
func (s *Service) authenticate(client *ClientService) bool {
|
||||
var (
|
||||
Authed = false
|
||||
Hasher = sha3.New256()
|
||||
UserPass string
|
||||
ServicePass string
|
||||
AuthRequest struct {
|
||||
Head struct {
|
||||
Type string `json:"Type"`
|
||||
} `json:"Head"`
|
||||
|
||||
Body struct {
|
||||
Pass string `json:"Password"`
|
||||
} `json:"Body"`
|
||||
}
|
||||
AuthResponse = map[string]map[string]interface{}{
|
||||
"Head": {
|
||||
"Type": HeadRegister,
|
||||
},
|
||||
"Body": {
|
||||
"Success": false,
|
||||
},
|
||||
}
|
||||
Response []byte
|
||||
)
|
||||
|
||||
err := client.Conn.ReadJSON(&AuthRequest)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read JSON message from websocket service client: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
if AuthRequest.Head.Type == HeadRegister {
|
||||
|
||||
Hasher.Write([]byte(AuthRequest.Body.Pass))
|
||||
UserPass = hex.EncodeToString(Hasher.Sum(nil))
|
||||
Hasher.Reset()
|
||||
|
||||
Hasher.Write([]byte(s.Config.Password))
|
||||
ServicePass = hex.EncodeToString(Hasher.Sum(nil))
|
||||
Hasher.Reset()
|
||||
|
||||
if UserPass == ServicePass {
|
||||
logger.Debug("Service client authenticated")
|
||||
Authed = true
|
||||
}
|
||||
|
||||
AuthResponse["Body"]["Success"] = Authed
|
||||
Response, err = json.Marshal(AuthResponse)
|
||||
if err != nil {
|
||||
logger.Error("Failed marshaling response: " + err.Error())
|
||||
}
|
||||
|
||||
client.Mutex.Lock()
|
||||
err := client.Conn.WriteMessage(websocket.TextMessage, Response)
|
||||
client.Mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed to write message: " + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return Authed
|
||||
}
|
||||
|
||||
return Authed
|
||||
}
|
||||
|
||||
// the main service routine
|
||||
func (s *Service) routine(client *ClientService) {
|
||||
|
||||
for {
|
||||
var (
|
||||
_, data, err = client.Conn.ReadMessage()
|
||||
response = make(map[string]map[string]any)
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("Failed to read JSON message from websocket service client: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("data:" + string(data))
|
||||
|
||||
err = json.Unmarshal(data, &response)
|
||||
if err != nil {
|
||||
logger.Error("Failed to unmarshal websocket response: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.dispatch(response, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) dispatch(response map[string]map[string]any, client *ClientService) {
|
||||
logger.Debug("Dispatch response:", response)
|
||||
switch response["Head"]["Type"] {
|
||||
|
||||
case HeadRegisterAgent:
|
||||
data, err := json.Marshal(response["Body"]["Agent"])
|
||||
if err != nil {
|
||||
logger.Error("Failed to marshal object to json: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var a = NewAgentService(data, client)
|
||||
if a == nil {
|
||||
logger.Error("Failed to make a new service agent.")
|
||||
return
|
||||
}
|
||||
|
||||
a.service = s
|
||||
|
||||
s.Agents = append(s.Agents, a)
|
||||
|
||||
pk := events.Service.AgentRegister(string(data))
|
||||
|
||||
s.Events.EventAppend(pk)
|
||||
s.Events.EventBroadcast("", pk)
|
||||
|
||||
logger.Debug(a.Json())
|
||||
|
||||
case HeadAgent:
|
||||
// TODO: find agent and send response to it
|
||||
|
||||
switch response["Body"]["Type"] {
|
||||
|
||||
case BodyAgentTask:
|
||||
var (
|
||||
Agent map[string]any
|
||||
Task string
|
||||
)
|
||||
|
||||
if val, ok := response["Body"]["Agent"]; ok && val != nil {
|
||||
Agent = val.(map[string]any)
|
||||
}
|
||||
|
||||
if val, ok := response["Body"]["Task"]; ok {
|
||||
Task = val.(string)
|
||||
} else {
|
||||
logger.Debug("response BodyAgentTask doesn't contain Task")
|
||||
return
|
||||
}
|
||||
|
||||
if Task == "Add" {
|
||||
logger.Debug("Adding task to TasksQueue")
|
||||
logger.Debug(Agent)
|
||||
|
||||
for index := range s.Data.ServerAgents.Agents {
|
||||
logger.Debug(fmt.Sprintf("AgentID:[%v] NameID:[%v]", Agent["NameID"], s.Data.ServerAgents.Agents[index].NameID))
|
||||
|
||||
if Agent["NameID"] == s.Data.ServerAgents.Agents[index].NameID {
|
||||
logger.Debug("Command =>", response["Body"]["Command"])
|
||||
var Command, err = base64.StdEncoding.DecodeString(response["Body"]["Command"].(string))
|
||||
if err != nil {
|
||||
logger.Error("Failed to decode command response: " + err.Error())
|
||||
}
|
||||
|
||||
var TaskJob = agent.Job{
|
||||
Payload: Command,
|
||||
}
|
||||
|
||||
s.Data.ServerAgents.Agents[index].AddJobToQueue(TaskJob)
|
||||
}
|
||||
}
|
||||
} else if Task == "Get" {
|
||||
logger.Debug("Get tasks queue")
|
||||
|
||||
if _, ok := response["Body"]["TasksQueue"]; !ok {
|
||||
for index := range s.Data.ServerAgents.Agents {
|
||||
logger.Debug(fmt.Sprintf("AgentID:[%v] NameID:[%v]", Agent["NameID"], s.Data.ServerAgents.Agents[index].NameID))
|
||||
|
||||
if Agent["NameID"] == s.Data.ServerAgents.Agents[index].NameID {
|
||||
logger.Debug("Found agent")
|
||||
var (
|
||||
TasksQueue = s.Data.ServerAgents.Agents[index].GetQueuedJobs()
|
||||
PayloadBuffer []byte
|
||||
)
|
||||
|
||||
for _, task := range TasksQueue {
|
||||
PayloadBuffer = append(PayloadBuffer, task.Payload...)
|
||||
}
|
||||
|
||||
response["Body"]["TasksQueue"] = base64.StdEncoding.EncodeToString(PayloadBuffer)
|
||||
|
||||
client.Mutex.Lock()
|
||||
logger.Debug("Write to websocket")
|
||||
err := client.Conn.WriteJSON(response)
|
||||
client.Mutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
logger.Debug("Failed to write json to service client: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.Data.ServerAgents.Agents[index].Info.LastCallIn = time.Now().Format("02-01-2006 15:04:05")
|
||||
|
||||
logger.Debug("Wrote to the client")
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
case BodyAgentRegister:
|
||||
logger.Debug("BodyAgentRegister")
|
||||
var (
|
||||
Size int
|
||||
MagicValue string
|
||||
AgentID string
|
||||
Header = agent.Header{}
|
||||
RegisterInfo = response["Body"]["RegisterInfo"].(map[string]any)
|
||||
AgentInstance *agent.Agent
|
||||
err error
|
||||
)
|
||||
|
||||
logger.Debug(RegisterInfo)
|
||||
|
||||
if val, ok := response["Body"]["AgentHeader"].(map[string]any)["Size"]; ok {
|
||||
if Size, err = strconv.Atoi(val.(string)); err != nil {
|
||||
Size = 0
|
||||
}
|
||||
Header.Size = Size
|
||||
}
|
||||
|
||||
if val, ok := response["Body"]["AgentHeader"].(map[string]any)["MagicValue"]; ok {
|
||||
MagicValue = val.(string)
|
||||
}
|
||||
|
||||
if val, ok := response["Body"]["AgentHeader"].(map[string]any)["AgentID"]; ok {
|
||||
AgentID = val.(string)
|
||||
}
|
||||
|
||||
MagicValue64, err := strconv.ParseInt(MagicValue, 16, 32)
|
||||
if err != nil {
|
||||
logger.Error("MagicValue64: " + err.Error())
|
||||
}
|
||||
|
||||
AgentID64, err := strconv.ParseInt(AgentID, 16, 32)
|
||||
if err != nil {
|
||||
logger.Error("MagicValue64: " + err.Error())
|
||||
}
|
||||
|
||||
Header.AgentID = int(AgentID64)
|
||||
Header.MagicValue = int(MagicValue64)
|
||||
|
||||
logger.Debug(Header)
|
||||
|
||||
AgentInstance = agent.AgentRegisterInfoToInstance(Header, RegisterInfo)
|
||||
|
||||
AgentInstance.Info.MagicValue = Header.MagicValue
|
||||
// AgentInstance.Info.Listener = h
|
||||
|
||||
s.TeamAgents.AppendAgent(AgentInstance)
|
||||
pk := events.Demons.NewDemon(AgentInstance)
|
||||
s.Events.EventAppend(pk)
|
||||
s.Events.EventBroadcast("", pk)
|
||||
|
||||
break
|
||||
|
||||
case BodyAgentResponse:
|
||||
logger.Debug("BodyAgentResponse")
|
||||
logger.Debug(response)
|
||||
|
||||
var RandID string
|
||||
|
||||
if val, ok := response["Body"]["RandID"]; ok {
|
||||
logger.Debug("Found RandID")
|
||||
RandID = val.(string)
|
||||
} else {
|
||||
logger.Debug("RandID not found")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(s.clients)
|
||||
for _, c := range s.clients {
|
||||
logger.Debug("c.Responses:", c.Responses)
|
||||
if channel, ok := c.Responses[RandID]; ok {
|
||||
logger.Debug("Found channel: " + RandID)
|
||||
|
||||
if val, ok := response["Body"]["Response"]; ok {
|
||||
var (
|
||||
resp []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if resp, err = base64.StdEncoding.DecodeString(val.(string)); err != nil {
|
||||
logger.Debug("Failed to decode base64: " + err.Error())
|
||||
}
|
||||
|
||||
channel <- resp
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
case BodyAgentOutput:
|
||||
var (
|
||||
AgentID = response["Body"]["AgentID"].(string)
|
||||
Callback = response["Body"]["Callback"].(map[string]any)
|
||||
)
|
||||
|
||||
if Callback["MiscType"] == "download" {
|
||||
|
||||
var (
|
||||
FileName = Callback["FileName"].(string)
|
||||
ContentB64 = Callback["Content"].(string)
|
||||
)
|
||||
|
||||
if FileContent, err := base64.StdEncoding.DecodeString(ContentB64); err == nil {
|
||||
|
||||
FileName = strings.Replace(FileName, "\x00", "", -1)
|
||||
|
||||
logger.Debug(fmt.Sprintf("Added downloaded file %v to agent directory: %v", FileName, AgentID))
|
||||
logr.LogrInstance.DemonAddDownloadedFile(AgentID, FileName, FileContent)
|
||||
Callback = make(map[string]any)
|
||||
|
||||
Callback["MiscType"] = "download"
|
||||
Callback["MiscData"] = ContentB64
|
||||
Callback["MiscData2"] = base64.StdEncoding.EncodeToString([]byte(FileName)) + ";" + common.ByteCountSI(int64(len(FileContent)))
|
||||
|
||||
} else {
|
||||
logger.Debug("Failed to decode FileContent base64: " + err.Error())
|
||||
|
||||
Callback = make(map[string]any)
|
||||
Callback["Type"] = "Error"
|
||||
Callback["Message"] = "Failed to decode FileContent base64: " + err.Error()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
out, _ = json.Marshal(Callback)
|
||||
pk = events.Demons.DemonOutput(AgentID, agent.HAVOC_CONSOLE_MESSAGE, string(out))
|
||||
)
|
||||
|
||||
s.Events.EventAppend(pk)
|
||||
s.Events.EventBroadcast("", pk)
|
||||
break
|
||||
|
||||
case BodyAgentBuild:
|
||||
var (
|
||||
ClientID = response["Body"]["ClientID"].(string)
|
||||
Message = response["Body"]["Message"].(map[string]any)
|
||||
)
|
||||
|
||||
if len(ClientID) > 0 {
|
||||
|
||||
if _, ok := Message["FileName"]; ok {
|
||||
var (
|
||||
FileName = Message["FileName"].(string)
|
||||
PayloadMsg = Message["Payload"].(string)
|
||||
Payload []byte
|
||||
err error
|
||||
)
|
||||
|
||||
Payload, err = base64.StdEncoding.DecodeString(PayloadMsg)
|
||||
if err != nil {
|
||||
err = s.Events.SendEvent(ClientID, events.Gate.SendConsoleMessage("Error", "Failed to decode base64 payload: "+err.Error()))
|
||||
if err != nil {
|
||||
logger.Error("Couldn't send Event: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = s.Events.SendEvent(ClientID, events.Gate.SendStageless(FileName, Payload))
|
||||
if err != nil {
|
||||
logger.Error("Error while sending event: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
var (
|
||||
MessageType = Message["Type"].(string)
|
||||
MessageText = Message["Message"].(string)
|
||||
)
|
||||
|
||||
err := s.Events.SendEvent(ClientID, events.Gate.SendConsoleMessage(MessageType, MessageText))
|
||||
if err != nil {
|
||||
logger.Error("Couldn't send Event: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.Error("ClientID not specified")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"Havoc/pkg/agent"
|
||||
"Havoc/pkg/events"
|
||||
"Havoc/pkg/profile"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type ClientService struct {
|
||||
Conn *websocket.Conn
|
||||
Responses map[string]chan []byte
|
||||
Mutex sync.Mutex
|
||||
}
|
||||
|
||||
type TeamAgentsInterface interface {
|
||||
AppendAgent(agent *agent.Agent) []*agent.Agent
|
||||
}
|
||||
|
||||
type ConfigService struct {
|
||||
Endpoint string
|
||||
Name string
|
||||
Password string
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
engine *gin.Engine
|
||||
clients []*ClientService
|
||||
|
||||
Config profile.ServiceConfig
|
||||
|
||||
Events events.EventInterface
|
||||
TeamAgents TeamAgentsInterface
|
||||
Agents []*AgentService
|
||||
Data struct {
|
||||
ServerAgents *agent.Agents
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
HeadRegister = "Register"
|
||||
HeadRegisterAgent = "RegisterAgent"
|
||||
HeadAgent = "Agent"
|
||||
|
||||
BodyAgentRegister = "AgentRegister"
|
||||
BodyAgentTask = "AgentTask"
|
||||
BodyAgentResponse = "AgentResponse"
|
||||
BodyAgentOutput = "AgentOutput"
|
||||
BodyAgentBuild = "AgentBuild"
|
||||
)
|
||||
@@ -123,6 +123,7 @@ brew install --cask cmake
|
||||
brew install python@3.10 qt@5 spdlog golang
|
||||
brew link --overwrite qt@5
|
||||
```
|
||||
|
||||
### <a name="bclient"></a>Building the Client
|
||||
|
||||
> If you are using a debian-based distro, you can use the bundled installation script at `/Havoc/Client/Install.sh`.
|
||||
@@ -142,9 +143,6 @@ make
|
||||
|
||||
> On macOS, run `brew unlink qt && brew link qt` after cmake build is done.
|
||||
|
||||
|
||||
|
||||
|
||||
### <a name="bteam"></a>Building the Teamserver
|
||||
|
||||
Install additional Go dependencies:
|
||||
@@ -196,7 +194,7 @@ Run the container:
|
||||
`sudo docker run -p8080:8080 -it -d -v havoc-c2-data:/data havoc-client`
|
||||
|
||||
Visit Jenkins at `localhost:8080` and create a pipeline to build the Havoc Teamserver.
|
||||
- See `Havoc-Teamserver.groovy` in the `Assets` folder.
|
||||
- See `Havoc-Teamserver.groovy` in the `assets` folder.
|
||||
|
||||
|
||||
### <a name="aquick"></a> Creating a Listener, and Spawning an Agent
|
||||
@@ -204,25 +202,25 @@ This part assumes you have a Teamserver running, with a Teamserver-client connec
|
||||
- Creating a Listener:
|
||||
1. To create a new listener, we must first open the `Listeners` subwindow.
|
||||
* To do this, in the upper left hand corner, click on the `View` button, and then on the `Listeners` button in the drop down menu.
|
||||
* 
|
||||
* 
|
||||
2. You should see a new sub window in the bottom of the server window, with the title of `Listeners` on the header tab.
|
||||
3. You should also now see three(3) buttons on the bottom of the server window, `Add`, `Remove` and `Edit`.
|
||||
* 
|
||||
* 
|
||||
4. We want to click the `Add` button.
|
||||
5. Once we click the `Add` button, you should see a new window come up, with the title of `Create Listener`.
|
||||
* 
|
||||
* 
|
||||
6. We will want to fill out the appropriate information for each field in the `Create Listener` window.
|
||||
7. After entering the appropriate information into each field, then click the `Save` button.
|
||||
8. The window will close, and you will now see a new line in the `Listeners` sub-window.
|
||||
* 
|
||||
* 
|
||||
9. We now have an active Listener, and are ready to receive an incoming agent's communications!
|
||||
- Spawning an Agent:
|
||||
1. To create an Agent Payload, we must first open the `Payload` window.
|
||||
* We can do so by going up to the upper left hand corner, and clicking on the `Attack` button.
|
||||
2. Doing so, we see the `Payload` button appear in the drop down menu. We want to then click on it.
|
||||
* 
|
||||
* 
|
||||
3. This will open the `Payload` window, where we may then configure the various options for generating our payload.
|
||||
* 
|
||||
* 
|
||||
4. Once we have selected the appropriate options, we then click on the `Generate` button.
|
||||
5. It might take a little bit for the compilation to take place. Once it has completed, it will prompt you as to where to save the resulting file output.
|
||||
6. After selecting where to save the file, you will now have a generated agent ready for execution or injection!
|
||||
@@ -387,12 +385,12 @@ Running `./Havoc` will start the Client.
|
||||
|
||||
When the client opens, you will be presented with a profile window similar to that in other C2 frameworks like Cobalt Strike.
|
||||
|
||||

|
||||

|
||||
|
||||
Enter the profile name, teamserver bind address (`Host`) and `Port`, along with your defined username/password in the teamserver profile.
|
||||
Then hit 'Connect' to connect your configured teamserver.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 310 KiB After Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 217 KiB After Width: | Height: | Size: 217 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 673 KiB After Width: | Height: | Size: 673 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -131,7 +131,6 @@ set(
|
||||
Source/Havoc/Service.cpp
|
||||
|
||||
Source/Havoc/DBManger/DBManager.cpp
|
||||
Source/Havoc/DBManger/Listener.cpp
|
||||
Source/Havoc/DBManger/Credentials.cpp
|
||||
Source/Havoc/DBManger/Sessions.cpp
|
||||
Source/Havoc/DBManger/Teamserver.cpp
|
||||
@@ -151,7 +150,7 @@ set(
|
||||
Source/Havoc/Havoc.cpp
|
||||
|
||||
${HAVOC_UI}
|
||||
Include/Havoc/PythonApi/PyAgentClass.hpp Source/Havoc/PythonApi/PyAgentClass.cpp)
|
||||
Include/Havoc/PythonApi/PyAgentClass.hpp Source/Havoc/PythonApi/PyAgentClass.cpp Include/json.hpp)
|
||||
|
||||
add_executable( ${PROJECT_NAME} ${HAVOC_INCLUDE} ${HAVOC_SOURCE} ${HAVOC_UTIL} ${APP_ICON_RESOURCE_WINDOWS} Data/Havoc.rc )
|
||||
add_definitions( -DQT_NO_DEBUG_OUTPUT )
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 376 B After Width: | Height: | Size: 376 B |
|
Before Width: | Height: | Size: 340 B After Width: | Height: | Size: 340 B |
|
Before Width: | Height: | Size: 536 B After Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 398 B After Width: | Height: | Size: 398 B |
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 395 B |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 515 B After Width: | Height: | Size: 515 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |