Shawn's Blog

一个伪程序员的伪技术博客

0X00 前言

设计一个系统,不论是 Web 还是其他的什么形式,通过用户名和密码认证也是一个再正常不过的事情了。但是如何保存密码却是一个值得讨论的问题,相信各位最开始的一个有用户名和密码的程序多半也是用明文存储的密码吧 🤓

这里总结了六种比较常用于密码存储的方式,接下来可以逐一进行简单的分析以帮助我们更好的保护用户的密码

一清二白:明文存储密码,直接存 password

掩耳盗铃:使用 BASE64 之类的编码,存储为 cGFzc3dvcmQ=

盘古之法:使用早已不再安全的 md5 之类的摘要算法,存储为

5f4dcc3b5aa765d61d8327deb882cf99

祖宗之法:使用也已经不再安全的 sha-1 之类的摘要算法,存储为

5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8

凑合能用:使用现代的安全的例如 sha-256 之类额算法,存储为

5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8

现代手段:采用系统化的多次迭代的加盐哈希方式加密,例如常见的 PBKDF2

有兴趣的话可以把我上面的这几个 md5/sha-1/sha-256 拿来到 CrackStation 尝试解一下,就知道弱密码究竟有多危险了。

不过在开始之前要先进行一些简单的科普,以便于能够比较好的理解后面的东西。

名词解释

拖库:简单来说就是有不怀好意的小老弟获取了你数据库的访问权限,甚至将其 dump 然后下载下来了
撞库:当你有某个库的用户名和密码明文了,其实可以拿着用户名密码去其他系统也尝试登录,因为很多人的用户名和密码在各个系统都是一样的,比如同一个邮箱注册的密码也用的完全相同
碰撞:一般是说 hash 碰撞,在有一对原文和对应的 hash 值后再找到一个 hash 值相同但原文不同的的方法
:加盐是一种在进行散列前向原文指定位置插入内容,将散列前的原文变得很长从而使彩虹表等方式失效的行为

密码破解

常见的几种破解密码的方式有:暴力破解、用字典的暴力破解、彩虹表、社会工程学。

其中暴力破解最为暴力(废话噢),就是把所有可能的密码组合一个个的尝试一直尝试到天荒地老。如果是一个 8 位密码且只由大小写数字构成的话也有 62^8=218340105584896 这么多种组合,就算每秒钟能算出来十万个也需要七十年才能完全遍历一遍;
其次是用字典,字典就是将常用的弱密码、生日这种存起来,暴力破解的时候不再尝试所有组合,而是从字典中逐个取出来尝试,速度较快但是成功率降低了,毕竟只尝试了可能性最大的一小部分密码;
彩虹表也是类似于字典,但是字典每条只是一个密码,彩虹表是密码明文与 hash 之后的 key-value 对,当数据库被拖/泄露了之后,可以尝试拿泄露出来的 hash 值来反查密码原文;
社会工程学听起来好像很野,但是谁还没有过用生日和手机号作为密码解密的尝试呢 🤣

阅读全文 »

0X00 前言

本篇文章是这篇「使用 git stash save 将暂存区命名」的重置版。因为根据 Google 的统计数据我得知某些问题的关键词搜索出来之后我的博客排行会比较靠前,所以把最容易被各位点击到的文章做了个重置计划,改进之前的一些不足,争取能够说的更清楚一些,也能节省各位一点点时间,希望能真正的帮助到从搜索引擎点进来的各位~

0X01 极度精炼的使用说明

可以使用 git stash save "message" 的方式为 stash 起来的变动命名,方便后面再次使用。
git-stash-save

stash 起来过后可以使用 git stash list 来查看已经被 stash 的列表,这里可以看到已经有两条了,值得注意的是 stash 的 id 每次都在更新,最近 stash 的是 0,1 就是上次 stash 的,以此类推git-stash-list

如果需要从新应用某个 stash 的改动,可以使用 git stash apply STASH_ID 的方式,例如使用 git stash apply 1 就可以重新应用 id 为 1 的这个 stash。如果要丢弃掉某个 stash 的话使用 git stash drop STASH_ID 就可以了

0X00 换个方式定义函数

本篇内容不严格区分 function 与 method 🥹

我们都知道在 Python 中如何定义一个函数,只需要 def foo(arg_1, arg_2, *args, **kwargs)
就足够了。知道的稍微多一些呢可能知道「Python 中万物皆对象,所以函数的调用也只是调用了函数对象中的 __call__
方法」,所以我们可以尝试用这种方式调用一个函数

1
2
3
4
5
6
7
def say_hello():
print('hello, world')

say_hello.__call__()

# output
# hello, world

既然可以这样调用了,我们也就可以用类似的方法来定义一个假的
function,可以发现我们自定义了随便一个类,但是只要它实现了 __call__ 方法就可以被当做函数一样调用

1
2
3
4
5
6
7
8
class Foo:
def __call__(self):
print('hello, world')

foo = Foo()foo()

# output
# hello, world

0X01 callable

根据上面的方法可知我们可以用 hasattr(obj, '__call__')
来判断某个对象是不是函数,事实上我也确实在同事的代码里看到过这样用的。其实 Python 内置了一个名为 callable
的函数可以用,不过跟 hasattr(obj, '__call__') 并不完全一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Foo:
pass


class Bar:
def __call__(self):
pass

func = lambda x : x


print(hasattr(Foo, '__call__'), callable(Foo))
# output: (False, True)

print(hasattr(Foo(), '__call__'), callable(Foo()))
# output: (False, False)

print(hasattr(Bar, '__call__'), callable(Bar))
# output: (True, True)

print(hasattr(Bar(), '__call__'), callable(Bar()))
# output: (True, True)

print(hasattr(func, '__call__'), callable(func))
# output: (True, True)

测试代码的第一行 Foo 类因为没有实现 __call__ 方法所以 hasattr 返回的是
False,而它是一个类,调用它就会实例化一个对象出来,所以它是可调用的,所以 callable 就返回了 True;第二行
Foo 类也没有实现 __call__ 方法所以 hasattr 返回的是 False,而且它又只是个普通对象,不是一个 class
所以导致 callable 也返回了 False;第三行因为 Bar 类实现了 __call__ 方法所以 hasattrcallable
都返回了 True;第四行也同理;第五行本是一个函数,所以也都返回了 True

需要注意的是官方文档提到「callable 返回了 True 的不一定真的能调用成功,但是返回 False
的一定不能成功」,比如你强行给某个类设置了一个 __call__
但是又不是函数,可能就会出现这样的问题。不过你非要这么写的话,小心被同事打死噢 🤔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo:
def __init__(self):
self.__call__ = '???'

foo = Foo()print(hasattr(foo, '__call__'))
print(callable(foo))
foo()

# output
# True
# True
# Traceback (most recent call last):
# File "hello.py", line 8, in <module>
# foo()
# TypeError: 'str' object is not callable

相关的官方文档:https://docs.python.org/3/library/functions.html#callable

0X00 可以被强制转换的自定义类

但凡写过 Python
的人应该都用过int()这个函数了,而且也都知道这个是将其他类型转换成int类型的内置方法,稍微用的多一点的还会知道这个方法如果传入不能被强制转换的数据时会抛出TypeError的异常。那你知道如何让自己定义的类可以被强制转换吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3


class A:
def __int__(self):
return 233


a = A()
print(int(a))

# output
# 233

而且按照官方文档来说的话,如果你的class定义了__int__()方法,则int(your_obj)则会返回__int__()的值,如果定义了__index__()则会返回__index__(),如果定义了__trunc__()也会返回__trunc__()。当然是有优先级的,优先级`int

index > trunc`,可以使用如下代码分别注释这些方法测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3


class A:
def __int__(self):
return 1

def __index__(self):
return 2

def __trunc__(self):
return 3


a = A()
print(int(a))

0X01 int 的第二个参数

那你知道它其实还能接收第二个参数吗?其实 int()
方法可以接受第二个参数的,也就是用于进制转换的参数。换言说就是可以用内置的int()方法将其他进制的字符串数据转换成10进制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python3


print(int('12345')) # 将字符串格式10进制数字转为整型
print(int('12345', base=10)) # base 默认就是10
print(int('12345', base=8)) # 8进制的12345转成10进制
print(int('FFFFF', base=16)) # 16进制的转成10进制
print(int('0XDEADBEEF', 16)) # 当然可以带对应的0X前缀
print(int('011111', 2)) # 将2进制的数字转成10进制

# output
# 12345
# 12345
# 5349
# 1048575
# 3735928559
# 31

0X02 hex、 bin 等其他转换方法

上面提到了进制转换,这里也就顺便说一下这两个方法好了。其中hex可以将整型数字转成0x开头的16进制字符串,bin可以将整形数字转成0b开头的2进制字符串

1
2
3
4
5
6
7
#!/usr/bin/env python3


print(hex(12345))
print(hex(3735928559))
print(bin(12345))
print(bin(23333))

还有几个之前从来不知道,这次写博客才在官方文档看到的用法,不仅可以控制大小写还能控制是否展示0X这种标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [1]: '%#x' % 255, '%x' % 255, '%X' % 255
Out[1]: ('0xff', 'ff', 'FF')

In [2]: format(255, '#x'), format(255, 'x'), format(255, 'X')
Out[2]: ('0xff', 'ff', 'FF')

In [3]: f'{255:#x}', f'{255:x}', f'{255:X}'
Out[3]: ('0xff', 'ff', 'FF')

In [4]: format(14, '#b'), format(14, 'b')
Out[4]: ('0b1110', '1110')

In [5]: f'{14:#b}', f'{14:b}'
Out[5]: ('0b1110', '1110')

In [6]: '%#o' % 10, '%o' % 10
Out[6]: ('0o12', '12')

In [7]: format(10, '#o'), format(10, 'o')
Out[7]: ('0o12', '12')

In [8]: f'{10:#o}', f'{10:o}'
Out[8]: ('0o12', '12')

官方文档参考:

https://docs.python.org/zh-cn/3/library/functions.html#int

https://docs.python.org/zh-cn/3/library/functions.html#hex

https://docs.python.org/zh-cn/3/library/functions.html#bin

https://docs.python.org/zh-cn/3/library/functions.html#oct

0X00 最常见的持久化方式:挂载出来

相信各位学习使用 Docker 的时候都会出现过好不容易用 Docker 启动了个数据库容器,然后发现只要容器消失之后数据也就一起消失了的情况。然后通常来说会使用这么一个方法来解决问题:将宿主机的某个目录挂载到容器里,这样一来那个数据库容器就可以将数据内容和配置持久化地存储下来了。一般会使用这么一个方法将某个目录挂载到容器内部 docker run -dit --name new_container --mount type=bind,source=/Users/shawn/Downloads/test/test_dir,target=/test_dir alpine

下面图中首先创建了一个名为 this_container 的容器,并将其 /test_dir 目录和宿主机的 /Users/shawn/Downloads/test/test_dir 目录绑定到一起了。然后在容器里向 /test_dir 写入文件之后发现在宿主机的对应位置也是可以看到的,并且文件不会因为容器的生命周期结束而被删除(如果数据在容器内部的话当容器被删除后自然也就不在了)。

将目录挂载到容器内部

这种方式其实更适合宿主机和容器需要共享一些文件的时候使用,例如我本地的开发环境在容器里但是代码又在宿主机上的情况就很适合这种方案。当然也可以将数据文件、配置文件等通过这种方式挂载出来。一般使用 Docker 部署各类服务的时候,都会将配置文件通过这种方式共享出来,方便我们在宿主机上修改配置然后在容器内部生效。

0X01 另一种持久化的方式:卷

说来惭愧,我一直以为只有上面一种数据持久化的方式,直到昨天看书的时候才知道 Docker 还有一种叫做 Volume 的存在。如果说上面说的将数据挂载出来到宿主机的方式有点类似与宿主机与虚拟机共享目录的话,那通过 Volume 持久化文件就类似于给虚拟机挂载一个虚拟磁盘了。

我们首先可以通过 docker volume ls 的方式检查一下当前环境是不是存在已经创建好了的 Volume,通常来说如果你的 Docker 环境已经用过一段时间并且创建过各种不同的容器的话那很有可能已经有存在的 Volume 了。接下来我们通过 docker run -dit --name volatiner --mount source=bizvol,target=/vol alpine 来启动一个容器,后面 mount 的参数就是将名为 bizvol 的 Volume 挂载到容器的 /vol 目录上。这里值得注意的一点是即使你没有手动创建这个名为 bizvol 的 Volume 也是可以正常启动这个容器的,Docker 会为我们自动创建这个 bizvol Volume 的。接下来再使用 docker volume ls 就可以看到这个被创建好了的 Volume 了,我们尝试着向其中写入一点点数据,然后删除这个容器,可以发现这个 Volume 还是健在的,此时我们再启动一个新的容器使用这个 Volume 是没问题的,甚至启动多个容器同时挂载都是可以的,这些容器里都能正常读写该 Volume 的数据。

使用 Volume

关于 Volume 有如下几个命令,这里简单介绍一下

1
2
3
4
5
docker volume create    # 创建 Volume
docekr volume ls # 查看当前存在的 Volume
docker volume inspect # 检查 Volume 的详细信息
docker volume prune # 删除未被容器或服务使用的全部 Volume
docker volume rm # 删除指定 Volume