Obsah
1. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
2. Nejjednodušší fuzzery bez zpětné vazby
3. Základ pro vlastní fuzzer – nástroj radamsa
4. „Mutace“ dat podporované nástrojem radamsa
5. Ukázky základních možností mutace dat
6. Vygenerování dat pro otestování textových vstupů
8. Příprava pro testování REST API
9. Otestování HTTP serveru pomocí nástroje ffuf
10. Vygenerování série náhodně zvolených koncových bodů
11. Upravený HTTP server zobrazující tělo požadavku
12. Vygenerování pseudonáhodných dat poslaných v těle požadavku
13. Vygenerování JSONu s pseudonáhodnými daty pro otestování HTTP serveru
14. Třída RandomPayloadGenerator
15. Modul pro mutaci (fuzzing) vstupního souboru ve formátu JSON
17. Využití vygenerovaných dat
19. Repositář s demonstračními příklady
1. Úvod do problematiky fuzzingu a fuzz testování – složení vlastního fuzzeru
V předchozí dvojici článků [1] [2] a fuzzingu a fuzzerech jsme se zpočátku zabývali spíše teoriemi, na kterých jsou tyto technologie založeny. Taktéž jsme si ukázali alespoň základní způsoby využití fuzzeru nazvaného go-fuzz, který je určen pro testování aplikací vyvinutých v programovacím jazyce Go. Dnes se budeme zabývat poněkud odlišnou kategorií fuzzerů – bude se jednat o nástroje určené pro testování REST API, tedy mj. i takových koncových bodů (endpoints), které jako svůj vstup akceptují data uložená do formátu JSON, popř. XML. Používat budeme převážně programovací jazyk Python, protože fuzzery pochopitelně nejsou ani zdaleka omezeny jen na programovací jazyk Go.
2. Nejjednodušší fuzzery bez zpětné vazby
Vlastní fuzzer je možné sestavit rozličnými způsoby. Buď můžeme použít již existující nástroje a využívat jejich možnosti, nebo se skutečně můžeme pokusit vytvořit si vlastní (zpočátku pravděpodobně ten nejjednodušší možný) fuzzer, který bude přesně odpovídat požadavkům programátora. Dnes si ukážeme obě možnosti, ovšem nejprve si připomeňme, na základě jakých kategorií je možné jednotlivé typy fuzzerů rozlišit:
- Jakým způsobem jsou generovány vstupy použité v testech a jak je vůbec specifikováno, o jaká data se má jednat.
- Zda fuzzer zná a nějakým způsobem využívá informace o vnitřní struktuře testovaného systému či nikoli (rozdělení je na black-box, white-box a gray-box testování).
- A dále podle toho, zda a jak fuzzer rozumí struktuře vstupních dat či zda jen pseudonáhodně generuje vstupy bez dalších potřebných znalostí či zpětné vazby (ta je mnohdy důležitá pro vytvoření minimální sady vstupů, které způsobí chybu).
- Zda fuzz testy zjišťují pokrytí (coverage) a upravují podle toho svoji sadu testovacích dat (korpus). Obecně se jedná o nejrychlejší cestu k nalezení chyby.
3. Základ pro vlastní fuzzer – nástroj radamsa
Nejprve se seznámíme s již existujícím nástrojem, který je primárně určen pro generování vstupních dat na základě předloženého vzoru. Tato data jsou využitelná dalšími nástroji, popř. přímo použitelná při jednoduchém testování. Tento nástroj, který se jmenuje radamsa, je vlastně tím nejjednodušším fuzzerem, jenž pouze generuje sekvenci pseudonáhodných dat na základě zadaných kritérií, ovšem neobsahuje žádnou zpětnou vazbu typu „tudy už nechoď“ či „pokus se tento chybný vstup zkrátit“. Z tohoto ohledu se tedy jedná o nástroj mnohem primitivnější než minule zmíněný program go-fuzz.
Ukažme si nyní překlad a instalaci . Ta je snadná a vyžaduje pouze minimum vývojářských nástrojů – překladač gcc, nástroj Make a klienta Gitu. Celý postup překladu a instalace vypadá následovně:
$ git clone https://gitlab.com/akihe/radamsa.git $ cd radamsa $ make $ sudo make install
Průběh stažení a instalace:
Cloning into 'radamsa'... remote: Enumerating objects: 154, done. remote: Counting objects: 100% (154/154), done. remote: Compressing objects: 100% (90/90), done. remote: Total 1724 (delta 89), reused 114 (delta 61), pack-reused 1570 Receiving objects: 100% (1724/1724), 461.26 KiB | 684.00 KiB/s, done. Resolving deltas: 100% (1062/1062), done. Checking connectivity... done. test -x bin/ol || make bin/ol make[1]: Entering directory `/home/tester/temp/out/radamsa' test -f ol.c.gz || wget -O ol.c.gz https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz || curl -L -o ol.c.gz https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz --2020-03-10 20:58:46-- https://gitlab.com/owl-lisp/owl/uploads/92375620fb4d570ee997bc47e2f6ddb7/ol-0.1.21.c.gz Resolving gitlab.com (gitlab.com)... 35.231.145.151 Connecting to gitlab.com (gitlab.com)|35.231.145.151|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 321839 (314K) [application/x-gzip] Saving to: ‘ol.c.gz’ 100%[=============================================================================>] 321 839 497KB/s in 0,6s 2020-03-10 20:58:48 (497 KB/s) - ‘ol.c.gz’ saved [321839/321839] gzip -d < ol.c.gz > ol.c mkdir -p bin cc -Wall -O3 -o bin/ol ol.c make[1]: Leaving directory `/home/tester/temp/out/radamsa' bin/ol -O1 -o radamsa.c rad/main.scm mkdir -p bin cc -Wall -O3 -o bin/radamsa radamsa.c
Po překladu (doufejme že úspěšném) postačuje přejít do podadresáře bin a spustit:
$ ./radamsa --help Usage: radamsa [arguments] [file ...] -h | --help, show this thing -a | --about, what is this thing? -V | --version, show program version -o | --output <arg>, output pattern, e.g. out.bin /tmp/fuzz-%n.%s, -, :80 or 127.0.0.1:80 or 127.0.0.1:123/udp [-] -n | --count <arg>, how many outputs to generate (number or inf) [1] -s | --seed <arg>, random seed (number, default random) -m | --mutations <arg>, which mutations to use [ft=2,fo=2,fn,num=5,td,tr2,ts1,tr,ts2,ld,lds,lr2,li,ls,lp,lr,lis,lrs,sr,sd,bd,bf,bi,br,bp,bei,bed,ber,uw,ui=2,xp=9,ab] -p | --patterns <arg>, which mutation patterns to use [od,nd=2,bu] -g | --generators <arg>, which data generators to use [random,file=1000,jump=200,stdin=100000] -M | --meta <arg>, save metadata about generated files to this file -r | --recursive, include files in subdirectories -S | --seek <arg>, start from given testcase -T | --truncate <arg>, take only first n bytes of each output (mainly intended for UDP) -d | --delay <arg>, sleep for n milliseconds between outputs -l | --list, list mutations, patterns and generators -C | --checksums <arg>, maximum number of checksums in uniqueness filter (0 disables) [10000] -H | --hash <arg>, hash algorithm for uniqueness checks (stream, sha1 or sha256) [stream] -v | --verbose, show progress during generation
Popř. můžeme – přesně podle návodu – zkusit jedinou „mutaci“ vstupních dat:
$ echo "HAL 9000" | ./radamsa HAL 1701411834604692317316873037158827154
4. „Mutace“ dat podporované nástrojem radamsa
Nástroj radamsa, který jsme právě přeložili a nainstalovali, pracuje tak, že postupně „mutuje“ vstupní data, a to s využitím mnoha různých algoritmů, z nichž některé jsou určeny pro vytváření textových vstupů pro testy a jiné pro vstupy binární. Všechny podporované algoritmy lze společně s jejich možnými parametry vypsat následujícím příkazem:
$ ./radamsa -l
Poslední verze radamsy by měla vypsat tyto algoritmy:
Mutations (-m) ab: enhance silly issues in ASCII string data handling bd: drop a byte bf: flip one bit bi: insert a random byte br: repeat a byte bp: permute some bytes bei: increment a byte by one bed: decrement a byte by one ber: swap a byte with a random one sr: repeat a sequence of bytes sd: delete a sequence of bytes ld: delete a line lds: delete many lines lr2: duplicate a line li: copy a line closeby lr: repeat a line ls: swap two lines lp: swap order of lines lis: insert a line from elsewhere lrs: replace a line with one from elsewhere td: delete a node tr2: duplicate a node ts1: swap one node with another one ts2: swap two nodes pairwise tr: repeat a path of the parse tree uw: try to make a code point too wide ui: insert funny unicode num: try to modify a textual number xp: try to parse XML and mutate it ft: jump to a similar position in block fn: likely clone data between similar positions fo: fuse previously seen data elsewhere nop: do nothing (debug/test) Mutation patterns (-p) od: Mutate once nd: Mutate possibly many times bu: Make several mutations closeby once Generators (-g) stdin: read data from standard input if no paths are given or - is among them file: read data from given files random: generate random data
5. Ukázky základních možností mutace dat
Díky tomu, že radamsa nabízí mnoho typů mutace dat, je možné vytvářet pseudonáhodné vstupy pro různé typy testů. V prvním příkladu se vstup, kterým je řetězec „param=123“ podrobí libovolné mutaci, a to celkem pětkrát:
$ echo "param=123" | ./radamsa -n 5 param=xyzzy par am=123 param=-4294967297 param=340282366920938463463374607431768211457 param=ʳ256
Ve druhém příkladu určíme, že se má použít algoritmus měnící pouze číselné hodnoty ze vstupu:
$ echo "param=123" | ./radamsa -n 5 -m num param=-0 param=-14 param=-207 param=65536 param=124
Další příklad vygeneruje testy pro jazyk Python:
$ echo "print(1+2**3)" | ./radamsa -n 10 -m num print(1+4294967294**3) print(9240853959469937641+0**1) print(1+2147483648**3) print(1+1**256) print(-1540+2**3) print(1+2**0) print(0+18446744073709581601**-18446744073709551612) print(170141183460469231731687303715884105727+-2986**1) print(150+2**65537) print(1+2**3)
Testy si pochopitelně můžete spustit, i když jejich vykonání bude trvat velmi dlouho:
$ echo "print(1+2**3)" | ./radamsa -n 10 -m num > test.py $ python3 test.py
Testy pro nástroj bc:
$ echo "1+(2*3)-(4)" | ./radamsa -n 10 -m num 1+(2*2)-(4) 1+(-1*9223372036854775807)-(1) 1+(2*4)-(340282366920938463463374607431768211455) 65535+(18446744073709551617*3)-(32773) 0+(-257*253)-(4) 1+(65535*4628165)-(4294967295) 1+(9223372036854775808*10)-(170141183460469231731687303715884105728) 4294967296+(--340282366920938463463374607349120052059*3)-(-24536405709273) 0+(2147483651*1)-(2) 1+(2*3)-(4)
Poslání testů přímo do bc:
$ echo "1+(2*3)-(4)" | ./radamsa -n 10 -m num | bc 18 0 9 384 18446744073709617150 4 2006146606 1020847100762815390436240682475606796917 32763 61657
6. Vygenerování dat pro otestování textových vstupů
Zajímavé jsou i možnosti, které nástroj radamsa nabízí při generování pseudonáhodných dat vhodných pro otestování textových vstupů. V dalším příkladu se slovo „password“ mutuje takovým způsobem, že se některé jeho znaky nebo i sekvence znaků opakují:
$ echo "password" | ./radamsa -n 5 -m sr passwordordordordordordordordordordordordordordordordordordordordordordordordordordord passwordordordordordordord passworrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrd ppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppassssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssword passwordddddddddddd
Naopak je možné vstup (opět slovo „password“ postupně zmenšovat mazáním náhodně vybraných znaků:
$ echo "password" | ./radamsa -n 5 -m sd pswo pa passwod password
Kombinace obou možností:
$ echo "password" | ./radamsa -n 5 -m sd -m sr pppppppppppppppppppasssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssword passwordrd ppassswowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowowoword passsssssssswordordordordordordordordordordordordordordordordordordordordorororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororororordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordordord pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppapassworrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrd
Další algoritmus mutace dat, tentokrát založený na permutaci:
$ echo "password" | ./radamsa -n 5 -m bp pasrws odpodaw srspasdsrw opassodrw wro spdas
Generování numerických dat, ovšem jen z číslovek 1–6:
$ echo "123456" | ./radamsa -n 10 -m bp 12 3456123456 613524 2 13546124536 621 453124 36513 64251354 62513246
7. Víceřádková textová data
Šablony mohou být specifikovány i v souborech. Následující příklad vytvoří sérii pěti souborů s pseudonáhodnými daty odvozenými od šablon uložených v souborech „example1.txt“ a „example2.txt“:
$ ./radamsa -n 5 -o %n.txt example1.txt example2.txt
Ve vstupním textovém souboru lze prohazovat řádky a vytvořit tak například sérii vstupů pro otestování překladače:
$ ./radamsa http_server.go -m ls
Řádky lze pochopitelně i mazat, a to naprosto stejným způsobem:
$ ./radamsa http_server.go -m ld
Kombinace s předchozími možnostmi:
$ ./radamsa http_server.go -m ld -m bp
Skutečně váš program dokáže zpracovat Unicode?:
$ echo "hello" | ./radamsa -n 10 -m ui
Ideální vstup pro programy napsané v céčku:
$ echo "hello" | ./radamsa -n 10 -m ab
8. Příprava pro testování REST API
Ve druhé polovině článků se budeme zabývat testováním REST API. Připravíme si tedy jednoduchý HTTP server, který bude zpracovávat všechny endpointy a bude na požadavky odpovídat stále stejným řetězcem. Vzhledem k tomu, že jsme již minule použili jazyk Go, napíšeme i webový server v tomto programovacím jazyce:
package main import ( "io" "log" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { log.Println(request.URL) io.WriteString(writer, "Hello world!\n\n") } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8080", nil) }
Spustíme ho:
$ go run http_server.go
A otestujeme:
$ curl -v localhost:8080 * Rebuilt URL to: localhost:8080/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 8080 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:8080 > Accept: */* > < HTTP/1.1 200 OK < Date: Wed, 11 Mar 2020 19:24:29 GMT < Content-Length: 14 < Content-Type: text/plain; charset=utf-8 < Hello world! * Connection #0 to host localhost left intact
9. Otestování HTTP serveru pomocí nástroje ffuf
Nyní již můžeme náš HTTP server otestovat, a to s použitím nástroje nazvaného ffuf. Ten lze nainstalovat přímo z dodávaného tarballu dostupného na adrese https://github.com/ffuf/ffuf/releases/tag/v1.0.2. Nejdříve si připravíme vstupní soubor obsahující seznam všech koncových bodů (soubor může obsahovat i parametry endpointů atd.):
endpoint1 endpoint2 foo/bar foo/bar/baz
Tento seznam uložíme do souboru pojmenovaného „endpoints.txt“. Následně spustíme nástroj fuff, kterému se musí předat jak jméno souboru, tak i adresa testovaného HTTP serveru. Povšimněte si, že se na konci adresy nachází řetězec „FUFF“. Tento řetězec je postupně nahrazován řetězci načítanými ze souboru „endpoints.txt“:
$ ffuf -w endpoints.txt -u http://localhost:8080/FUZZ
Průběh celého testování vypadá následovně:
/'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v1.1.0-git ________________________________________________ :: Method : GET :: URL : http://localhost:8080/FUZZ :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403 ________________________________________________ endpoint2 [Status: 200, Size: 14, Words: 2, Lines: 3] [Status: 200, Size: 14, Words: 2, Lines: 3] foo/bar/baz [Status: 200, Size: 14, Words: 2, Lines: 3] foo/bar [Status: 200, Size: 14, Words: 2, Lines: 3] endpoint1 [Status: 200, Size: 14, Words: 2, Lines: 3] :: Progress: [5/5] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::
Můžeme vidět, že se vytvořilo celkem pět požadavků a na všechny tyto požadavky server odpověděl HTTP kódem 200 OK, což je v pořádku (ostatně by bylo podezřelé, kdyby tak malý HTTP server zhavaroval).
10. Vygenerování série náhodně zvolených koncových bodů
Nyní si vytvoříme soubor nazvaný „example.txt“ a uložíme do něj jediný řádek:
endpoint
Z tohoto souboru je možné vygenerovat libovolný počet koncových bodů (endpointů):
$ ./radamsa example.txt -n 5 endpoint endp�oint mpoint nendpoi�nt eodpoendpoint
Takto vytvořené koncové body by bylo možné použít pro testování HTTP serveru, ovšem ve skutečnosti není nutné nástroj radamsa volat přímo. Můžeme ho totiž zavolat automaticky přes ffuf – nyní se pseudonáhodné endpointy připojí na konec URL namísto řetězce „FUZZ“:
$ ffuf --input-cmd './radamsa example.txt' -u http://localhost:8080/FUZZ
Taktéž lze snadno vytvářet pseudonáhodné hodnoty parametru/parametrů:
$ ffuf --input-cmd './radamsa example.txt' -u http://localhost:8080/parameter1=FUZZ
11. Upravený HTTP server zobrazující tělo požadavku
Náš HTTP server můžeme nepatrně upravit a vylepšit takovým způsobem, aby zobrazoval těla požadavků (takzvaný payload). Nová varianta serveru může vypadat následovně:
package main import ( "io" "io/ioutil" "log" "net/http" ) func mainEndpoint(writer http.ResponseWriter, request *http.Request) { log.Println(request.URL) body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("Error reading body: %v", err) http.Error(writer, "can't read body", http.StatusBadRequest) return } log.Println(string(body)) io.WriteString(writer, "Hello world!\n\n") } func main() { http.HandleFunc("/", mainEndpoint) http.ListenAndServe(":8080", nil) }
Server spustíme:
$ go run http_server_post.go
12. Vygenerování pseudonáhodných dat poslaných v těle požadavku
V tomto okamžiku již můžeme využít kombinaci nástrojů ffuf a radamsa pro otestování koncového bodu HTTP serveru, kterému se předají data v těle požadavku (tedy metodou POST). Povšimněte si, že náhodnou hodnotu bude mít parametr „name“:
$ ffuf --input-cmd './radamsa values.txt' -u http://localhost:8080/ -X POST -H "Content-Type: application/json" -d '{"name": "FUZZ", "anotherkey": "anothervalue"}' -fr "error"
Průběh testování:
/'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v1.1.0-git ________________________________________________ :: Method : POST :: URL : http://localhost:8080/ :: Header : Content-Type: application/json :: Data : {"name": "FUZZ", "anotherkey": "anothervalue"} :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200,204,301,302,307,401,403 :: Filter : Regexp: error ________________________________________________ 1 [Status: 200, Size: 14, Words: 2, Lines: 3] 2 [Status: 200, Size: 14, Words: 2, Lines: 3] 3 [Status: 200, Size: 14, Words: 2, Lines: 3] 4 [Status: 200, Size: 14, Words: 2, Lines: 3] ... ... ... 96 [Status: 200, Size: 14, Words: 2, Lines: 3] 97 [Status: 200, Size: 14, Words: 2, Lines: 3] 98 [Status: 200, Size: 14, Words: 2, Lines: 3] 99 [Status: 200, Size: 14, Words: 2, Lines: 3] 100 [Status: 200, Size: 14, Words: 2, Lines: 3] :: Progress: [100/100] :: Job [1/1] :: 50 req/sec :: Duration: [0:00:02] :: Errors: 0 ::
13. Vygenerování JSONu s pseudonáhodnými daty pro otestování HTTP serveru
V následujících dvou kapitolách si ukážeme jednoduchou utilitu určenou pro vygenerování hodnot ve formátu JSON s pseudonáhodnými daty, která lze využít pro otestování HTTP serveru. Tato utilitka je umístěna v repositáři na adrese https://github.com/tisnik/payload-fuzzer a skládá se ze dvou zdrojových souborů:
- random_payload_generator.py
- json_fuzzer.py
14. Třída RandomPayloadGenerator
Ve třídě RandomPayloadGenerator je implementován generátor náhodných dat, a to včetně seznamů a slovníků. A právě různým způsobem vnořené seznamy a slovníky tvoří struktury vhodné pro serializaci do formátu JSON:
"""Generator of random payload for testing API.""" import string import random class RandomPayloadGenerator: """Generator of random payload for testing API.""" def __init__(self): """Initialize the random payload generator.""" self.iteration_deep = 0 self.max_iteration_deep = 2 self.max_dict_key_length = 10 self.max_string_length = 20 self.dict_key_characters = string.ascii_lowercase + string.ascii_uppercase + "_" self.string_value_characters = (string.ascii_lowercase + string.ascii_uppercase + "_" + string.punctuation + " ") def generate_random_string(self, n, uppercase=False, punctuations=False): """Generate random string of length=n.""" prefix = random.choice(string.ascii_lowercase) mix = string.ascii_lowercase + string.digits if uppercase: mix += string.ascii_uppercase if punctuations: mix += string.punctuation suffix = ''.join(random.choice(mix) for _ in range(n - 1)) return prefix + suffix def generate_random_key_for_dict(self, data): """Generate a string key to be used in dictionary.""" existing_keys = data.keys() while True: new_key = self.generate_random_string(10) if new_key not in existing_keys: return new_key def generate_random_list(self, n): """Generate list filled in with random values.""" return [self.generate_random_payload((int, str, float, bool, list, dict)) for i in range(n)] def generate_random_dict(self, n): """Generate dictionary filled in with random values.""" dict_content = (int, str, list, dict) return {self.generate_random_string(10): self.generate_random_payload(dict_content) for i in range(n)} def generate_random_list_or_string(self): """Generate list filled in with random strings.""" if self.iteration_deep < self.max_iteration_deep: self.iteration_deep += 1 value = self.generate_random_list(5) self.iteration_deep -= 1 else: value = self.generate_random_value(str) return value def generate_random_dict_or_string(self): """Generate dict filled in with random strings.""" if self.iteration_deep < self.max_iteration_deep: self.iteration_deep += 1 value = self.generate_random_dict(5) self.iteration_deep -= 1 else: value = self.generate_random_value(str) return value def generate_random_value(self, type): """Generate one random value of given type.""" generators = { str: lambda: self.generate_random_string(20, uppercase=True, punctuations=True), int: lambda: random.randrange(100000), float: lambda: random.random() * 100000.0, bool: lambda: bool(random.getrandbits(1)), list: lambda: self.generate_random_list_or_string(), dict: lambda: self.generate_random_dict_or_string() } generator = generators[type] return generator() def generate_random_payload(self, restrict_types=None): """Generate random payload with possibly restricted data types.""" if restrict_types: types = restrict_types else: types = (str, int, float, list, dict, bool) selected_type = random.choice(types) return self.generate_random_value(selected_type)
15. Modul pro mutaci (fuzzing) vstupního souboru ve formátu JSON
Druhý zdrojový soubor obsahuje modul provádějící mutaci (či fuzzing) vstupního souboru ve formátu JSON. Ze vstupního souboru jsou vytvořeny tři nové třídy souborů:
- JSON s odstraněnými položkami
- JSON s přidanými pseudonáhodnými položkami
- JSON s mutovanými pseudonáhodnými položkami
Následuje výpis zdrojového kódu tohoto modulu:
import json import os import sys import itertools import copy import random from random_payload_generator import RandomPayloadGenerator output_num = 0 def load_json(filename): """Load and decode JSON file.""" with open(filename) as fin: return json.load(fin) def generate_output(payload): global output_num filename = "generated_{}.json".format(output_num) with open(filename, 'w') as f: json.dump(payload, f, indent=4) output_num += 1 def remove_items_one_iter(original_payload, items_count, remove_flags): keys = list(original_payload.keys()) # deep copy new_payload = copy.deepcopy(original_payload) for i in range(items_count): remove_flag = remove_flags[i] if remove_flag: key = keys[i] del new_payload[key] generate_output(new_payload) def fuzz_remove_items(original_payload): items_count = len(original_payload) # lexicographics ordering remove_flags_list = list(itertools.product([True, False], repeat=items_count)) # the last item contains (False, False, False...) and we are not interested # in removing ZERO items remove_flags_list = remove_flags_list[:-1] for remove_flags in remove_flags_list: remove_items_one_iter(original_payload, items_count, remove_flags) def add_items_one_iter(original_payload, how_many): # deep copy new_payload = copy.deepcopy(original_payload) rpg = RandomPayloadGenerator() for i in range(how_many): new_key = rpg.generate_random_key_for_dict(new_payload) new_value = rpg.generate_random_payload() new_payload[new_key] = new_value generate_output(new_payload) def fuzz_add_items(original_payload, min, max, mutations): for how_many in range(min, max): for i in range(1, mutations): add_items_one_iter(original_payload, how_many) def change_items_one_iteration(original_payload, how_many): # deep copy new_payload = copy.deepcopy(original_payload) rpg = RandomPayloadGenerator() for i in range(how_many): selected_key = random.choice(list(original_payload.keys())) new_value = rpg.generate_random_payload() new_payload[selected_key] = new_value generate_output(new_payload) def fuzz_change_items(original_payload, min, max): for how_many in range(1, len(original_payload)): for i in range(min, max): change_items_one_iteration(original_payload, how_many) def main(filename): original_payload = load_json(filename) fuzz_remove_items(original_payload) fuzz_add_items(original_payload, 1, 4, 2) fuzz_change_items(original_payload, 1, 4) if len(sys.argv) < 2: print("Usage: python json_fuzzer.py input_file.json") sys.exit(1) main(sys.argv[1])
16. Ukázka práce fuzzeru
Vstupní soubor, který bude tvořit základní šablonu dat, může vypadat následovně:
{ "name": "baf", "type": "fuzzer", "version": 2, "subversion": 0 }
Příklady vygenerovaných výsledků:
{ "type": "fuzzer" } { "subversion": 0, "version": 2, "t0i1nzhfih": "rXZY^w%Yr#p6Gk:!vpMs", "name": "baf", "type": "fuzzer", "qaao0f1o3d": 46589.933317956624 } { "subversion": 0, "name": "baf", "version": [ "d#7QZu,UCQc.;F8w/!B", [ 3572, "u|~n\"V$I+.(,b}bE,!", "uaKuZQL;FyIleF*!h(;", "g}P0h6o.UKFY8sIV_C%~", true ], "gerL~`EgPN1-})JjH\"L", 13805.792659417404, [ 31934.701190647098, "t8*!{B;c3ZFfg(aK[,\\", 13634, "i|L'vuP=Lk(:56\\uhVn", "h+7M5OtTM\\LcPPP3z)i" ] ], "type": "fuzzer" }
17. Využití vygenerovaných dat
Využití vygenerovaných dat je již jednoduché:
for file in *.json do echo $file curl -X POST -d @$file localhost:8080/ done
Jen pro malou ukázku je zde vypsána část logů serveru:
2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "subversion": 7168.838223613449, "name": false, "version": true, "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "subversion": 83238.604257801, "name": "tsvbG17xbD5NM1::!^>'", "version": 2, "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "version": 2} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "version": 2, "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "name": "baf"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "name": "baf", "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "name": "baf", "version": 2} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "name": "baf", "version": 2, "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "subversion": 0} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "subversion": 0, "type": "fuzzer"} 2020/03/11 21:21:03 / 2020/03/11 21:21:03 { "name": "baf", "type": "fuzzer", "version": 2, "subversion": 0}
18. Závěr
V dnešním článku jsme si ukázali dvě možnosti sestavení vlastního jednoduchého fuzzeru. První možnost spočívá v kombinaci radamsa+ffuf, druhá pak ve využití ffuf společně s jednoduchým projektem payload-fuzzer.
19. Repositář s demonstračními příklady
Zdrojové kódy všech dnes použitých demonstračních příkladů byly uloženy do Git repositáře, který je dostupný na adrese https://github.com/tisnik/fuzzing-examples. V případě, že nebudete chtít klonovat celý repositář (ten je ovšem – alespoň prozatím – velmi malý, dnes má přibližně šest až sedm megabajtů), můžete namísto toho použít odkazy na jednotlivé demonstrační příklady, které naleznete v následující tabulce:
# | Příklad | Stručný popis | Cesta |
---|---|---|---|
1 | http_server.go | jednoduchý HTTP server | https://github.com/tisnik/fuzzing-examples/blob/master/simple_server/http_server.go |
2 | http_server_post.go | HTTP server, který vypisuje celé tělo požadavku | https://github.com/tisnik/fuzzing-examples/blob/master/simple_server/http_server_post.go |
20. Odkazy na Internetu
- radamsa
https://gitlab.com/akihe/radamsa - Fuzzing (Wikipedia)
https://en.wikipedia.org/wiki/Fuzzing - american fuzzy lop
http://lcamtuf.coredump.cx/afl/ - Fuzzing: the new unit testing
https://go-talks.appspot.com/github.com/dvyukov/go-fuzz/slides/fuzzing.slide#1 - Corpus for github.com/dvyukov/go-fuzz examples
https://github.com/dvyukov/go-fuzz-corpus - AFL – QuickStartGuide.txt
https://github.com/google/AFL/blob/master/docs/QuickStartGuide.txt - Introduction to Fuzzing in Python with AFL
https://alexgaynor.net/2015/apr/13/introduction-to-fuzzing-in-python-with-afl/ - Writing a Simple Fuzzer in Python
https://jmcph4.github.io/2018/01/19/writing-a-simple-fuzzer-in-python/ - How to Fuzz Go Code with go-fuzz (Continuously)
https://fuzzit.dev/2019/10/02/how-to-fuzz-go-code-with-go-fuzz-continuously/ - Golang Fuzzing: A go-fuzz Tutorial and Example
http://networkbit.ch/golang-fuzzing/ - Fuzzing Python Modules
https://stackoverflow.com/questions/20749026/fuzzing-python-modules - 0×3 Python Tutorial: Fuzzer
http://www.primalsecurity.net/0×3-python-tutorial-fuzzer/ - fuzzing na PyPi
https://pypi.org/project/fuzzing/ - Fuzzing 0.3.2 documentation
https://fuzzing.readthedocs.io/en/latest/ - Randomized testing for Go
https://github.com/dvyukov/go-fuzz - HTTP/2 fuzzer written in Golang
https://github.com/c0nrad/http2fuzz - Ffuf (Fuzz Faster U Fool) – An Open Source Fast Web Fuzzing Tool
https://hacknews.co/hacking-tools/20191208/ffuf-fuzz-faster-u-fool-an-open-source-fast-web-fuzzing-tool.html - Continuous Fuzzing Made Simple
https://fuzzit.dev/ - Halt and Catch Fire
https://en.wikipedia.org/wiki/Halt_and_Catch_Fire#Intel_x86 - Pentium F00F bug
https://en.wikipedia.org/wiki/Pentium_F00F_bug - Random testing
https://en.wikipedia.org/wiki/Random_testing - Monkey testing
https://en.wikipedia.org/wiki/Monkey_testing - Fuzzing for Software Security Testing and Quality Assurance, Second Edition
https://books.google.at/books?id=tKN5DwAAQBAJ&pg=PR15&lpg=PR15&q=%22I+settled+on+the+term+fuzz%22&redir_esc=y&hl=de#v=onepage&q=%22I%20settled%20on%20the%20term%20fuzz%22&f=false - Z80 Undocumented Instructions
http://www.z80.info/z80undoc.htm - The 6502/65C02/65C816 Instruction Set Decoded
http://nparker.llx.com/a2/opcodes.html - libFuzzer – a library for coverage-guided fuzz testing
https://llvm.org/docs/LibFuzzer.html - fuzzy-swagger na PyPi
https://pypi.org/project/fuzzy-swagger/ - fuzzy-swagger na GitHubu
https://github.com/namuan/fuzzy-swagger - Fuzz testing tools for Python
https://wiki.python.org/moin/PythonTestingToolsTaxonomy#Fuzz_Testing_Tools - A curated list of awesome Go frameworks, libraries and software
https://github.com/avelino/awesome-go - gofuzz: a library for populating go objects with random values
https://github.com/google/gofuzz - tavor: A generic fuzzing and delta-debugging framework
https://github.com/zimmski/tavor - hypothesis na GitHubu
https://github.com/HypothesisWorks/hypothesis - Hypothesis: Test faster, fix more
https://hypothesis.works/ - Hypothesis
https://hypothesis.works/articles/intro/ - What is Hypothesis?
https://hypothesis.works/articles/what-is-hypothesis/ - Databáze CVE
https://www.cvedetails.com/ - Fuzz test Python modules with libFuzzer
https://github.com/eerimoq/pyfuzzer - Taof – The art of fuzzing
https://sourceforge.net/projects/taof/ - JQF + Zest: Coverage-guided semantic fuzzing for Java
https://github.com/rohanpadhye/jqf - http2fuzz
https://github.com/c0nrad/http2fuzz - Demystifying hypothesis testing with simple Python examples
https://towardsdatascience.com/demystifying-hypothesis-testing-with-simple-python-examples-4997ad3c5294