mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-03 14:06:51 +02:00
300 lines
8.7 KiB
Go
300 lines
8.7 KiB
Go
package script
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/gzip"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/base32"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash"
|
|
"html"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// registerCodec installs the encode/decode/hash/hmac/jwt/random helpers on the
|
|
// VM. These are the data-transform toolkit for scripts: base and binary
|
|
// encodings, hashing and keyed HMAC signing, JWT inspection, and secure random
|
|
// (nonces, PKCE, OAuth state). Output encoding for hash/hmac/random is selectable
|
|
// as "hex" (default), "base64", "base64url" or "base32".
|
|
func registerCodec(vm *goja.Runtime) {
|
|
vm.Set("encode", map[string]interface{}{
|
|
// text and url
|
|
"base64": func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) },
|
|
"base64url": func(s string) string { return base64.RawURLEncoding.EncodeToString([]byte(s)) },
|
|
"base32": func(s string) string { return base32.StdEncoding.EncodeToString([]byte(s)) },
|
|
"hex": func(s string) string { return hex.EncodeToString([]byte(s)) },
|
|
"url": func(s string) string { return url.QueryEscape(s) },
|
|
"urlPath": func(s string) string { return url.PathEscape(s) },
|
|
"html": func(s string) string { return html.EscapeString(s) },
|
|
"json": func(call goja.FunctionCall) goja.Value {
|
|
b, err := json.Marshal(call.Argument(0).Export())
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
return vm.ToValue(string(b))
|
|
},
|
|
// object -> application/x-www-form-urlencoded
|
|
"form": func(call goja.FunctionCall) goja.Value {
|
|
return vm.ToValue(formEncode(call.Argument(0)))
|
|
},
|
|
// compression, output as base64 so it survives as a JS string
|
|
"gzip": func(s string) (string, error) { return deflateEncode(s, true) },
|
|
"deflate": func(s string) (string, error) {
|
|
return deflateEncode(s, false)
|
|
},
|
|
})
|
|
|
|
vm.Set("decode", map[string]interface{}{
|
|
"base64": func(s string) (string, error) {
|
|
b, err := base64.StdEncoding.DecodeString(s)
|
|
return string(b), err
|
|
},
|
|
"base64url": func(s string) (string, error) {
|
|
b, err := decodeBase64URL(s)
|
|
return string(b), err
|
|
},
|
|
"base32": func(s string) (string, error) {
|
|
b, err := base32.StdEncoding.DecodeString(s)
|
|
return string(b), err
|
|
},
|
|
"hex": func(s string) (string, error) {
|
|
b, err := hex.DecodeString(s)
|
|
return string(b), err
|
|
},
|
|
"url": func(s string) (string, error) { return url.QueryUnescape(s) },
|
|
"urlPath": func(s string) (string, error) { return url.PathUnescape(s) },
|
|
"html": func(s string) string { return html.UnescapeString(s) },
|
|
"json": func(s string) (interface{}, error) {
|
|
var v interface{}
|
|
err := json.Unmarshal([]byte(s), &v)
|
|
return v, err
|
|
},
|
|
// application/x-www-form-urlencoded -> object
|
|
"form": func(s string) (map[string]interface{}, error) { return formDecode(s) },
|
|
"gzip": func(s string) (string, error) { return inflateDecode(s, true) },
|
|
"deflate": func(s string) (string, error) {
|
|
return inflateDecode(s, false)
|
|
},
|
|
})
|
|
|
|
// hashing: hash.sha256(input, enc?) -> string
|
|
vm.Set("hash", map[string]interface{}{
|
|
"md5": hashFn(vm, func() hash.Hash { return md5.New() }),
|
|
"sha1": hashFn(vm, func() hash.Hash { return sha1.New() }),
|
|
"sha256": hashFn(vm, func() hash.Hash { return sha256.New() }),
|
|
"sha384": hashFn(vm, func() hash.Hash { return sha512.New384() }),
|
|
"sha512": hashFn(vm, func() hash.Hash { return sha512.New() }),
|
|
})
|
|
|
|
// keyed HMAC: hmac.sha256(key, message, enc?) -> string
|
|
vm.Set("hmac", map[string]interface{}{
|
|
"sha1": hmacFn(vm, func() hash.Hash { return sha1.New() }),
|
|
"sha256": hmacFn(vm, func() hash.Hash { return sha256.New() }),
|
|
"sha384": hmacFn(vm, func() hash.Hash { return sha512.New384() }),
|
|
"sha512": hmacFn(vm, func() hash.Hash { return sha512.New() }),
|
|
})
|
|
|
|
// jwt.decode(token) -> { header, payload, signature } (NOT verified)
|
|
vm.Set("jwt", map[string]interface{}{
|
|
"decode": func(token string) (map[string]interface{}, error) { return jwtDecode(token) },
|
|
})
|
|
|
|
// random: nonces, PKCE verifiers, OAuth state
|
|
vm.Set("random", map[string]interface{}{
|
|
"bytes": func(call goja.FunctionCall) goja.Value {
|
|
n := int(call.Argument(0).ToInteger())
|
|
if n <= 0 || n > 4096 {
|
|
panic(vm.NewTypeError("random.bytes: length must be 1..4096"))
|
|
}
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
return vm.ToValue(codecOutput(b, argString(call, 1, "hex")))
|
|
},
|
|
"uuid": func() string { return uuid.NewString() },
|
|
})
|
|
}
|
|
|
|
// codecOutput renders bytes in the requested encoding, defaulting to hex.
|
|
func codecOutput(b []byte, enc string) string {
|
|
switch strings.ToLower(enc) {
|
|
case "base64":
|
|
return base64.StdEncoding.EncodeToString(b)
|
|
case "base64url":
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
case "base32":
|
|
return base32.StdEncoding.EncodeToString(b)
|
|
default:
|
|
return hex.EncodeToString(b)
|
|
}
|
|
}
|
|
|
|
// argString reads an optional string argument, falling back to def.
|
|
func argString(call goja.FunctionCall, i int, def string) string {
|
|
if len(call.Arguments) > i {
|
|
v := call.Argument(i)
|
|
if !goja.IsUndefined(v) && !goja.IsNull(v) {
|
|
return v.String()
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func hashFn(vm *goja.Runtime, newH func() hash.Hash) func(goja.FunctionCall) goja.Value {
|
|
return func(call goja.FunctionCall) goja.Value {
|
|
h := newH()
|
|
h.Write([]byte(call.Argument(0).String()))
|
|
return vm.ToValue(codecOutput(h.Sum(nil), argString(call, 1, "hex")))
|
|
}
|
|
}
|
|
|
|
func hmacFn(vm *goja.Runtime, newH func() hash.Hash) func(goja.FunctionCall) goja.Value {
|
|
return func(call goja.FunctionCall) goja.Value {
|
|
m := hmac.New(newH, []byte(call.Argument(0).String()))
|
|
m.Write([]byte(call.Argument(1).String()))
|
|
return vm.ToValue(codecOutput(m.Sum(nil), argString(call, 2, "hex")))
|
|
}
|
|
}
|
|
|
|
// decodeBase64URL decodes url-safe base64 with or without padding.
|
|
func decodeBase64URL(s string) ([]byte, error) {
|
|
s = strings.TrimRight(s, "=")
|
|
return base64.RawURLEncoding.DecodeString(s)
|
|
}
|
|
|
|
func deflateEncode(s string, gz bool) (string, error) {
|
|
var buf bytes.Buffer
|
|
var w io.WriteCloser
|
|
var err error
|
|
if gz {
|
|
w = gzip.NewWriter(&buf)
|
|
} else {
|
|
w, err = flate.NewWriter(&buf, flate.DefaultCompression)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if _, err := w.Write([]byte(s)); err != nil {
|
|
return "", err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(buf.Bytes()), nil
|
|
}
|
|
|
|
func inflateDecode(b64 string, gz bool) (string, error) {
|
|
data, err := base64.StdEncoding.DecodeString(b64)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var r io.ReadCloser
|
|
if gz {
|
|
r, err = gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
r = flate.NewReader(bytes.NewReader(data))
|
|
}
|
|
defer r.Close()
|
|
out, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(out), nil
|
|
}
|
|
|
|
// formEncode turns an object into an application/x-www-form-urlencoded string.
|
|
func formEncode(val goja.Value) string {
|
|
values := url.Values{}
|
|
if m, ok := val.Export().(map[string]interface{}); ok {
|
|
for k, v := range m {
|
|
switch vv := v.(type) {
|
|
case []interface{}:
|
|
for _, item := range vv {
|
|
values.Add(k, fmt.Sprint(item))
|
|
}
|
|
default:
|
|
values.Set(k, fmt.Sprint(v))
|
|
}
|
|
}
|
|
}
|
|
return values.Encode()
|
|
}
|
|
|
|
// formDecode parses a form-encoded string into an object. A key with one value
|
|
// becomes a string, a repeated key becomes an array.
|
|
func formDecode(s string) (map[string]interface{}, error) {
|
|
vals, err := url.ParseQuery(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]interface{}{}
|
|
for k, v := range vals {
|
|
if len(v) == 1 {
|
|
out[k] = v[0]
|
|
continue
|
|
}
|
|
arr := make([]interface{}, len(v))
|
|
for i, x := range v {
|
|
arr[i] = x
|
|
}
|
|
out[k] = arr
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// jwtDecode splits a JWT and returns its header and payload as objects plus the
|
|
// raw signature. It does NOT verify the signature: it is for inspecting tokens.
|
|
func jwtDecode(token string) (map[string]interface{}, error) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) < 2 {
|
|
return nil, fmt.Errorf("invalid jwt: expected header.payload.signature")
|
|
}
|
|
header, err := decodeB64URLJSON(parts[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("jwt header: %w", err)
|
|
}
|
|
payload, err := decodeB64URLJSON(parts[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("jwt payload: %w", err)
|
|
}
|
|
sig := ""
|
|
if len(parts) >= 3 {
|
|
sig = parts[2]
|
|
}
|
|
return map[string]interface{}{
|
|
"header": header,
|
|
"payload": payload,
|
|
"signature": sig,
|
|
}, nil
|
|
}
|
|
|
|
func decodeB64URLJSON(s string) (interface{}, error) {
|
|
b, err := decodeBase64URL(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var v interface{}
|
|
if err := json.Unmarshal(b, &v); err != nil {
|
|
return nil, err
|
|
}
|
|
return v, nil
|
|
}
|