223 lines
7.4 KiB
Go
223 lines
7.4 KiB
Go
// Package decoder реализует набор кодировок/декодировок (аналог вкладки
|
||
// Decoder в Burp): URL, Base64, HTML-entities, Hex и однонаправленный MD5,
|
||
// плюс SmartDecode — эвристическая цепочка автоматического декодирования,
|
||
// подбирающая, какая кодировка применена, и снимающая её слой за слоем.
|
||
package decoder
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/base64"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"html"
|
||
"net/url"
|
||
"strings"
|
||
)
|
||
|
||
// URLEncode кодирует строку как query-компонент URL (пробел → '+').
|
||
func URLEncode(s string) string {
|
||
return url.QueryEscape(s)
|
||
}
|
||
|
||
// URLDecode декодирует query-компонент URL.
|
||
func URLDecode(s string) (string, error) {
|
||
out, err := url.QueryUnescape(s)
|
||
if err != nil {
|
||
return "", fmt.Errorf("URL-декодирование: %w", err)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// Base64Encode кодирует в стандартный Base64 (с паддингом).
|
||
func Base64Encode(s string) string {
|
||
return base64StdEncode([]byte(s))
|
||
}
|
||
|
||
// Base64Decode декодирует Base64, принимая как стандартный, так и
|
||
// URL-safe алфавит и терпимо относясь к отсутствующему паддингу —
|
||
// на практике токены из веба встречаются в обоих вариантах.
|
||
func Base64Decode(s string) (string, error) {
|
||
b, err := base64FlexibleDecode(s)
|
||
if err != nil {
|
||
return "", fmt.Errorf("Base64-декодирование: %w", err)
|
||
}
|
||
return string(b), nil
|
||
}
|
||
|
||
// HTMLEncode экранирует HTML-спецсимволы (<, >, &, ", ').
|
||
func HTMLEncode(s string) string {
|
||
return html.EscapeString(s)
|
||
}
|
||
|
||
// HTMLDecode разворачивает HTML-сущности (<, &, ' и т.п.).
|
||
func HTMLDecode(s string) string {
|
||
return html.UnescapeString(s)
|
||
}
|
||
|
||
// HexEncode кодирует строку в шестнадцатеричное представление.
|
||
func HexEncode(s string) string {
|
||
return hex.EncodeToString([]byte(s))
|
||
}
|
||
|
||
// HexDecode декодирует шестнадцатеричную строку обратно в текст.
|
||
func HexDecode(s string) (string, error) {
|
||
b, err := hex.DecodeString(strings.TrimSpace(s))
|
||
if err != nil {
|
||
return "", fmt.Errorf("hex-декодирование: %w", err)
|
||
}
|
||
return string(b), nil
|
||
}
|
||
|
||
// base64StdEncode — тонкая обёртка над стандартной библиотекой,
|
||
// вынесенная в отдельную функцию ради симметрии с base64FlexibleDecode.
|
||
func base64StdEncode(b []byte) string {
|
||
return base64.StdEncoding.EncodeToString(b)
|
||
}
|
||
|
||
// base64FlexibleDecode перебирает стандартный и URL-safe алфавиты,
|
||
// с паддингом и без — то есть все четыре комбинации, которые реально
|
||
// встречаются в вебе (JWT, например, использует RawURLEncoding).
|
||
func base64FlexibleDecode(s string) ([]byte, error) {
|
||
encodings := []*base64.Encoding{
|
||
base64.StdEncoding,
|
||
base64.RawStdEncoding,
|
||
base64.URLEncoding,
|
||
base64.RawURLEncoding,
|
||
}
|
||
var lastErr error
|
||
for _, enc := range encodings {
|
||
if b, err := enc.DecodeString(s); err == nil {
|
||
return b, nil
|
||
} else {
|
||
lastErr = err
|
||
}
|
||
}
|
||
return nil, lastErr
|
||
}
|
||
|
||
// MD5Hash — однонаправленное хэширование, декодирования не существует.
|
||
func MD5Hash(s string) string {
|
||
sum := md5.Sum([]byte(s))
|
||
return hex.EncodeToString(sum[:])
|
||
}
|
||
|
||
// Step — один шаг цепочки SmartDecode: какая кодировка была снята
|
||
// и что получилось после этого шага.
|
||
type Step struct {
|
||
Operation string
|
||
Result string
|
||
}
|
||
|
||
// SmartDecode пытается определить, чем закодирована строка, и снимает
|
||
// кодировки слой за слоем, пока распознаётся хотя бы одна из известных
|
||
// (URL, Base64, Hex, HTML) или не достигнут предел итераций. Это
|
||
// эвристика, а не гарантированно верное определение — при неоднозначных
|
||
// входах (например, строка, которая одновременно валидный Base64 и
|
||
// валидный hex) приоритет фиксирован в порядке проверки ниже.
|
||
func SmartDecode(s string) []Step {
|
||
const maxSteps = 8
|
||
var steps []Step
|
||
|
||
cur := s
|
||
for i := 0; i < maxSteps; i++ {
|
||
op, next, ok := detectAndDecodeOne(cur)
|
||
if !ok || next == cur {
|
||
break
|
||
}
|
||
steps = append(steps, Step{Operation: op, Result: next})
|
||
cur = next
|
||
}
|
||
return steps
|
||
}
|
||
|
||
// detectAndDecodeOne проверяет кодировки по очереди и применяет первую
|
||
// подошедшую. Порядок проверки важен: URL-encoding и HTML-entities имеют
|
||
// узнаваемые маркеры (%XX, &...;) и проверяются первыми как самые
|
||
// однозначные; Base64 и Hex — по составу алфавита, где Hex строже
|
||
// (только 0-9a-f), поэтому его проверяем раньше Base64.
|
||
func detectAndDecodeOne(s string) (op, result string, ok bool) {
|
||
trimmed := strings.TrimSpace(s)
|
||
|
||
if strings.Contains(s, "%") && looksURLEncoded(s) {
|
||
if out, err := URLDecode(s); err == nil && out != s {
|
||
return "URL-decode", out, true
|
||
}
|
||
}
|
||
|
||
if strings.Contains(s, "&") && strings.Contains(s, ";") {
|
||
if out := HTMLDecode(s); out != s {
|
||
return "HTML-decode", out, true
|
||
}
|
||
}
|
||
|
||
if looksHex(trimmed) {
|
||
if out, err := HexDecode(trimmed); err == nil && isPrintable(out) {
|
||
return "Hex-decode", out, true
|
||
}
|
||
}
|
||
|
||
if looksBase64(trimmed) {
|
||
if out, err := Base64Decode(trimmed); err == nil && isPrintable(out) {
|
||
return "Base64-decode", out, true
|
||
}
|
||
}
|
||
|
||
return "", "", false
|
||
}
|
||
|
||
func looksURLEncoded(s string) bool {
|
||
for i := 0; i < len(s)-2; i++ {
|
||
if s[i] == '%' && isHexByte(s[i+1]) && isHexByte(s[i+2]) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isHexByte(c byte) bool {
|
||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
|
||
}
|
||
|
||
func looksHex(s string) bool {
|
||
if s == "" || len(s)%2 != 0 {
|
||
return false
|
||
}
|
||
for i := 0; i < len(s); i++ {
|
||
if !isHexByte(s[i]) {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func looksBase64(s string) bool {
|
||
if s == "" || len(s)%4 != 0 {
|
||
return false
|
||
}
|
||
for i := 0; i < len(s); i++ {
|
||
c := s[i]
|
||
switch {
|
||
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9', c == '+', c == '/', c == '-', c == '_', c == '=':
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// isPrintable — грубый фильтр "похоже на текст, а не на мусор", чтобы
|
||
// SmartDecode не радостно декодировал случайные бинарные данные,
|
||
// формально прошедшие проверку алфавита Base64/Hex, но не являющиеся
|
||
// осмысленной строкой на выходе.
|
||
func isPrintable(s string) bool {
|
||
if s == "" {
|
||
return false
|
||
}
|
||
printable := 0
|
||
for _, r := range s {
|
||
if r == '\n' || r == '\r' || r == '\t' || (r >= 0x20 && r < 0x7f) {
|
||
printable++
|
||
}
|
||
}
|
||
return float64(printable)/float64(len([]rune(s))) > 0.85
|
||
}
|