0%

最近在腾讯云购置了一台2C2G的服务器用来放置 Gitlab。但是发现 Gitlab 的 docker 镜像最少需要 4G 内存才能跑起来,所以尝试下启用 SWAP 来用硬盘空间代替内存空间

PS:腾讯云轻量服务器在内存消耗完毕时,会发生硬盘IO读写高的故障,导致无法通过SSH连接到服务器。笔者已经在两台服务器上测试过了,确实会发生这个BUG。

SWAP

windows 连接共享打印机时有可能发生的错误以及解决方案

0x00004005

解决方式为手工添加打印机

注意这个选项如何填写, 比如打印机共享在192.168.1.48下,像这样

那么我们应当填写的值为

1
\\192.168.1.48\EPSON L1800 Series

像这样

然后选择打印机对应的驱动, 可以通过 windows update 从 windows 下载,也可以自己下载然后选择从磁盘安装

然后我连接的是这个打印机,所以我选择这个驱动

设置一个打印机名字

这一步问你是否共享这个打印机,如果不共享,选第一个选项就 OK。

然后就设置好了, 可以打印个测试页测试一下。

https://www.youtube.com/watch?v=ZTYPBpGzB_g

Gitlab 里有一个配置选项 external_url

顾名思义这里配置的是从外部机器访问 Gitlab 的链接,可以是域名或ip地址

在配置这个选项的时候需要注意一个问题,就是这个配置的是里 gitlab 最近的访问路径。

比如我们有如下一个结构

基于 Docker 容器的结构

访客 -> nginx 容器 -> gitlab 容器

此时,访客假如在浏览器输入网址https://gitlab.wudinaonao.com,浏览器将请求发送给 nginx 容器,nginx容器通过反向代理连接到 gitlab 容器

这个时候就要注意了,external_url 这个选项并不能设置为https://gitlab.wudinaonao.com(并不是绝对的)

为什么?

因为在上述结构里 https://gitlab.wudinaonao.com 并不是离 gitlab 最近的访问路径, nginx 通过反向代理向 gitlab 发出的请求才是离 gitlab 最近的链接.

例如, 如果我们通过 docker-compose 来配置 nginx 和 docker 镜像.

1
2
3
4
5
6
7
8
9
10
version: "3.1"
services:
Gitlab:
# 省略其他配置 ...

Nginx:
# 省略其他配置 ...
depends_on:
- Gitlab

在上述容器组中, Nginx 容器可以通过 Gitlab 来访问 Gitlab 容器, 可以在 Nginx 容器里使用ping命令测试ping gitlab

那么在这样的结构里, external_url的值就应当配置为http://Gitlab

并不是绝对的

为什么设置成http://Gitlab并不是绝对的, 因为通过 Nginx 反向代理时, 其实可以更改请求头的 host 选项!

只需要将 hostexternal_url 匹配即可

基于动态DNS的结构

访客 -> 路由器 -> 内网主机

如果 gitlab 主机假设在一个私网里, 我们通过端口映射的方式来访问, 这种情况下, 我们需要设置

external_url 的值为 内网主机的 ip 地址

详细来说, 假设我们的路由器对外有一个公网ip, 我们通过动态dns的方式绑定了一个域名 abc.com, 然后我们将内网里的gitlab主机端口暴露出去, 比如为 8080

那么此时我们可以通过 http:abc.com:8080 的方式来访问内网的 gitlab 主机吗 ?

并不可以.

是因为 abc.com 并不是距离 gitlab 最近的请求, 所以我们需要设置 external_url 的值为 内网主机的 ip 地址

此时才可以正常访问.

https://blog.csdn.net/atlasun/article/details/115749373

问题描述

收了一个华硕 Z77 平台的 p8z77-v lx2, 但是使用 DP 线插在显卡(我的是980ti)上进入系统时黑屏,具体表现为无法看见 BIOS 启动画面。但是如果换成 HDMI,DVI 线则可以看到。

解决方案

英伟达有一个固件升级工具

https://us.download.nvidia.com/Windows/uefi/firmware/1.0/NVIDIA_DisplayPort_Firmware_Updater_1.0-x64.exe

下载该工具更新显卡固件即可解决

参考链接

https://diy.pconline.com.cn/1496/14961331.html

在使用 Docker 部署 Nextcloud 并通过 Nginx 反向代理时,会发生登陆后不跳转的问题

具体表现为上图,会一直卡在这里。

但是此时刷新页面可以看到登录后的页面。

解决

config/config.php 这个文件中添加

1
'overwriteprotocol' => 'https',

像这样

参考链接

https://wangzhengzhen.com/2499.html

前言

在 WEB 开发技术中, 字符串插值是一种很常用的操作, 在各种 Web 前端框架中都能看到其身影.

例如 VUE 中, {{ }} 用两个大括号包裹住可以表示一个表达式

1
<p>{{ name }}</p>

简言之, 此时我们有变量

1
name = "naonao"

那么上面的 HTML 模板最终会被渲染为

1
<p>naonao</p>

我们可以实现一个render函数, 像这样

1
2
def render(templates: str, values: Dict[str, str]) -> str:
...

目标

  • 接收一个模板字符串, 以及需要被替换的变量字典, 返回渲染后的结果字符串.
  • 当模板串里的变量不存在于变量字典时, 抛出异常.
  • {{ }} 包裹住的变量需要清除其前后的空白字符, 例如 {{ name }}, {{name}}, {{ name}} 都是等价的

特别的

1
2
{{{name}}}
{{ {name} {age} }}

这些也应当是有效变量, 他们最终会被识别为

1
2
"{name}"
"{name} {age}"

如何实现呢?

解决思路

  1. 先搜索出模板串里的变量
  2. 从变量字典里提取对应变量值并替换
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from typing import Dict, List, Tuple


def _search(string: str) -> Tuple[str, Tuple[int, int]]:
"""单次搜索模板串

Args:
string: 目标字符串

Return: 模板串, 以及在原始字符串中的起止坐标
当字符串里不存在插值模板时返回空值.
例 -> ('', (None, None))
"""
i = 0
s_list = list(string)
s_len = len(s_list)
_stack = []
_temp = []
_start = None
_end = None

while i < s_len:
# 仅查找第一个模板串
if _start is not None and _end is not None:
break

# 当栈里面的 { 符号数量大于等于 2 时,
# 表示当前指针已经指向"变量"字符串, 所以
# 需要开始记录
if len(_stack) >= 2:
_temp.append(s_list[i])

# 检测连续两个字符是否为 -> {{
if s_list[i] == "{":
try:
_next = s_list[i + 1]
except IndexError:
raise SyntaxError("格式不正确, 缺少 {")
if _next == "{":
_stack.append(s_list[i])
_stack.append(s_list[i + 1])
_temp.append(s_list[i])
_temp.append(s_list[i + 1])
_start = i
i += 2
continue
else:
# 不连续时仅添加
_stack.append(s_list[i])

# 消括号
if s_list[i] == "}":
# 消括号时, 如果辅助栈里剩余 2 个括号, 则
# 表示当前指向的字符与下一个字符一定是 -> }
# 即应当是一个连续的 }}, 否则格式不正确
if len(_stack) == 2:
try:
_next = s_list[i + 1]
except IndexError:
raise SyntaxError("格式不正确, 缺少 }")
# 栈里只剩下两个元素时, 一定要出现两个连续的
# }} 否则格式不正确
if _next != "}":
raise SyntaxError("格式不正确, 缺少 }")
# 清空栈并记录结束坐标
_stack.clear()
i += 2
_temp.append(_next)
_end = i
continue

# 如果栈里存在元素就弹一个出来
if _stack:
_stack.pop()

# 指针 + 1
i += 1

# 辅助栈不为空则说明格式不正确
if _stack:
raise SyntaxError("格式不正确, 缺少 }")

return ("".join(_temp), (_start, _end))

def _var_name(temp_string: str) -> str:
try:
var = temp_string[2:-2]
except IndexError:
raise SyntaxError("格式不正确")
return var.strip()

def _replace(string: str, k1: str, k2: str) -> str:
"""
Args:

string: 原始字符串
k1: 被替换的子串
k2: 替换后的子串

"""
s_list = list(string)
s_len = len(s_list)

k1, k2 = str(k1), str(k2)
k1_list, k2_list = list(k1), list(k2)
k1_len, k2_len = len(k1_list), len(k2_list)

res = list()

i = 0
_temp = 0
while i < s_len:
# i 指针指向的位置匹配到了 k1 的首元素
# 从该位置向后匹配, 检测是否完全匹配
if s_list[i] == k1_list[0]:
for j in range(k1_len):
if k1_list[j] == s_list[i + j]:
_temp += 1
else:
break

if _temp == k1_len:
res.extend(k2_list)
i += (k1_len - 1)
else:
res.append(s_list[i])

i += 1
_temp = 0

return "".join(res)

def render(string: str, values: Dict[str, Tuple[str, int]]) -> str:
"""渲染模板字符串

逻辑概要

string = "my {{ name }} is, {{ age }} !!"

查找到第一个模板串 "{{ name }}", 然后提取左串

l_string = "my {{ name }}"

将左串替换后的结果增加到 s_list, 然后更新 _temp 为右串

_temp = " is, {{ age }} !!"

直至搜索不到模板串, 替换完毕.
"""
s_list = list()
_temp = string
while True:
# 查找模板串里的模板
t_string, (i_start, i_end) = _search(_temp)
if i_start is None or i_end is None:
break

# 取模板字符的具体值
name = _var_name(t_string)
if name not in values:
raise ValueError(f"缺少关键字 {name}")
value = str(values[name])

# 截取左串(第一个搜索到的模板串), 将替换后的结果
# 添加到 s_list, 然后更新 _temp 变量为右串(原始字符串剩余部分)
l_string = _replace(_temp[: i_end], t_string, value)
s_list.append(l_string)
_temp = _temp[i_end:]

s_list.append(_temp)
return "".join(s_list)

验证下效果

1
2
3
4
5
6
7
8
strings = [
"{{ name }} is, {{ age }}",
"my {{ name }} is, {{ age }}",
"{{ name }} is, {{ age }} !!",
"my {{ name }} is, {{ age }} !!",
]
for string in strings:
print(f'|{render(string, {"name": "nao", "age": 16})}|')
1
2
3
4
|nao is, 16|
|my nao is, 16|
|nao is, 16 !!|
|my nao is, 16 !!|

达到预期效果!

介绍

Odoo Challenge 挑战赛是一个很有意思的项目, 起源于一次面试时, HR发来个链接说测试一下水平.

好了, 开始挑战

LEVEL 1

先随便输一个密码, 点 Check Answer, 很不意外的密码错误, 不过这时候有提示, 可以根据提示的线索来找到密码.

看到提示

1
Hint: if form['the_password'] == form['pwd']: return True

看看网页的源码, 猜测应该在里面能找到.

部分源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form method="POST" action="/jobs/challenge/submit" role="form">
<input class="d-none" name="csrf_token" value="6ad9541dcf313efc428fa7f50dda9ca079adfe4ao1687487891"/>
<div class="form-group row">
<label for="password" class="col-md-2 offset-md-2 col-form-label">Password</label>
<div class="col-md-6">

<input type="hidden" name="the_password" value="796ff3e72821cf412787389c6d5f301e00e3efb6"/>
<input type="text" required="required" class="form-control" name="pwd" id="password" placeholder=""/>

</div>
</div>
<div class="form-group">
<div>
<button id="submit" type="submit" name="signup" class="btn btn-primary">Check Answer</button>
</div>
</div>
</form>

注意到

1
<input type="hidden" name="the_password" value="796ff3e72821cf412787389c6d5f301e00e3efb6"/>
1
796ff3e72821cf412787389c6d5f301e00e3efb6

就是本关的密码了

LEVEL 2

第二关提示如下

1
Hint: function debug() { [native code] }

猜测应该是和 debug 有关系, 所以可以 F12 打开开发者工具在命令控制台标签页找线索

直接出答案了

LEVEL 3

提示

1
Hint: GET / HTTP/1.1

猜测应该在与服务器交互的请求里寻找.

我们随便输一个密码, 提交一下看看与服务器的交互请求都有哪些

每个请求都点开看一看 request / response 里面有没有什么可以寻找的线索.

可以看到在 next?wrong=oups 这个请求的响应头里面有内容, 红框标记的猜测应该是答案, 即

1
Grow your business with Odoo and more than 985156 apps

LEVEL 4

提示

1
Hint: Classy password

笔者在解决这个问题的时候花费了相当长的时间。最开始我猜测提示经典密码也许有可能是弱口令?

我试了如下弱口令

1
2
3
4
5
6
7
8
admin
root
123456
000000
888888
88888888
00000000
11111111

诸如此类的弱口令发现都没有用。

最后我写了一个脚本, 从 Github 上找了找有没有彩虹表, 常用弱口令之类的仓库.

找到一个 https://github.com/rootphantomer/Blasting_dictionary

挂着跑了一下午, 回来一看跑了大概 2w 个密码仍然没有任何起色. 这个时候我开始怀疑一开始的思路是否正确, 提示经典密码的意思究竟是不是弱口令的意思?

但是此时我又没有更好的思路, 于是每个请求都翻了翻, 看看有没有有用的信息.

真让我翻到了….

1
https://www.odoo.com/jobs/challenge/challenge.css

这个请求的内容就是密码….

呵呵, 踏破铁鞋无觅处…

这个事情告诉我什么道理呢, 如果一个题目看起来很简单, 但是通过暴力方法很难解决时, 一定要寻找其他的突破口….

LEVEL 5

提示

1
Hint: I'm a small piece of data sent from a website and stored on the user's computer

意思是服务器发送了一个数据片段存储到了用户电脑上.

看到这个首先想到 local storage 和 cookies

印证了我的想法, 我在请求

1
https://www.odoo.com/jobs/challenge/next?wrong=oups

里发现了线索, 密码既是方框里的值

1
Odoo-9850101554950100

PS: 事后我发现这个页面的图标是一个饼干.

cookies 的意思正是饼干. 看来还是要多观察不一样的细节.

LEVEL 6

给了一段 js 代码

1
2
3
4
5
6
7
8
9
10
// JS
var idx = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
var tmp = "";
for (var i=0; i < pwd.length; i++)
tmp += " "+idx.indexOf(pwd.charAt(i));

if(tmp == " 2 38 39 36 4 14 11 8 5 12 1 3 37 7 1 0 5 14 8 0")
{
$.post('/jobs/challenge/submit', {pwd: pwd}).always(function(x) { window.location = '/jobs/challenge/next'});
}

看来一下逻辑, tmp 变量存储的应该是 idx 字符串的索引, 我们把索引对应的字符取出来即可.

可以用 Python 实现一个简单的解码函数

1
2
3
4
idx = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
tmp = "2 38 39 36 4 14 11 8 5 12 1 3 37 7 1 0 5 14 8 0".split(" ")
pwd = "".join([idx[int(index)] for index in tmp])
print(pwd)

得到密文

1
2CDA4eb85c13B7105e80

本关通过

LEVEL 7

哈哈, 从现在开始难度增加了…..

提示

1
Hint: ascii

ascii 码? 这个提示…..简单到等于没什么提示…

还是去翻一翻请求, 看看 request / response / cookies 有什么线索吧.

首先在控制台看到打印了一串信息

翻译了一下大概是说需要调用另一个 URL 来执行下一个任务.

然后在请求

1
https://www.odoo.com/jobs/challenge/next?wrong=oups

里发现了不少信息.

这个地方有一个奇怪的信息, 我们先记一下.

name value
It-Is-The-Part-2-Of-Url “/jobs/challenge//1d1/prime.json”

请求头也有一个奇怪的信息

name value
It-Is-The-Part-1-Of-Url “/jobs/challenge/ebe//prime.json”

这时候我们可以分析, 这个 URL 被分为了两部分, 放到一起看一下

1
2
/jobs/challenge/<part1>/1d1/prime.json
/jobs/challenge/ebe/<part2>/prime.json

那么完整的 URL 应该是

1
/jobs/challenge/ebe/1d1/prime.json

我们尝试访问这个 URL

得到一串 JSON

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
{
"instructions": "Remove non prime numbers from data then right shift [222th decimal of the constant quantity that determines the area of a circle by multiplying it by the radius squared] bits from the remaining numbers, it will give you the alphanumeric password of the next level",
"numbers": [
52191,
25453,
13613,
54064,
25111,
22425,
12821,
19580,
25163,
46513,
40022,
57039,
9167,
13697,
26041,
47396,
13577,
11932,
50399,
14591,
13323,
46296,
20704,
53379,
24889,
22289,
24989,
51709,
47000,
294,
26249,
52972,
51191,
44807,
43176,
6419,
26161,
13339,
48093,
12547,
4258,
14479,
42054,
13913,
22828,
59633,
40834,
37100,
34715,
17108,
39104,
24790,
14407,
24967,
64736,
561,
38676,
14479,
17092,
62096,
42309,
26471,
59563,
25609,
13459,
2233,
14303,
52568,
50193,
28990,
34498,
25321,
21884,
26183,
13441,
25541,
57043,
4395,
25943,
25057,
41090,
14057,
38058,
7060,
26773,
37842,
25867,
33069,
8468,
46003,
24411,
20484,
19630,
3410,
35367,
26177,
25471,
35463,
12959,
25903,
25153,
13999,
44393,
25411,
2829,
51176,
25913,
13913,
52113,
17144,
14533
]
}

翻译一下, 大概意思是, 把这个数组里的非素数删除了, 然后将剩余的数字右移x位(也许是位运算?), 然后就得到了密码

x 位指的是应该是 pi 的第 222 位的数字

至此, 我们梳理一下完整的逻辑, 把上面给定的数组里的非素数删除, 然后找到 pi 的第 222 位的值 x, 然后将数组中剩余的数字按位运算右移 x 位.

网上找一个高效的判断是否位素数的函数

1
2
3
4
5
6
7
8
9
10
11
def is_prime(x: int) -> bool:
"""check if is prime
referencen link https://zhuanlan.zhihu.com/p/107300262"""
if x == 2 or x == 3:
return True
if x % 6 != 1 and x % 6 != 5:
return False
for i in range(5, int(x ** 0.5) + 1, 6):
if x % i == 0 or x % (i + 2) == 0:
return False
return True

查一下 pi 的第 222 位是几, 这里可以自己写函数计算也可以直接在网上查, 这里仅仅需要知道该值即可, 所以我选择了直接在网上查

直接在网站

1
http://pai.babihu.com/

查的 pi 的 222 位为 8

还记的最开始的提示吗?

1
Hint: ascii

这里运算完的应该是 ascii 码, 我们计算出对应的字符就得到了最后的密文, 至此我们开始编写解码函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

def is_prime(x: int) -> bool:
"""check if is prime
referencen link https://zhuanlan.zhihu.com/p/107300262"""
if x == 2 or x == 3:
return True
if x % 6 != 1 and x % 6 != 5:
return False
for i in range(5, int(x ** 0.5) + 1, 6):
if x % i == 0 or x % (i + 2) == 0:
return False
return True

def decrypt(arr: list) -> str:
# 移除非素数
arr = list(filter(lambda x: is_prime(x), arr))
# 将数组中的每个数字按位右移8, 按 ascii 码表取对应的字符
# 最后将字符数组拼接成字符串, 即可得到密码
dec = "".join(list(map(lambda x: chr(x >> 8), arr)))
return dec

解密得密码

1
c5b2b5e58aaff41868a8d7bf4cea6efc2eb6ce68

本关通过

LEVEL 8

本关注意到这个图片变得奇怪了, 事出反常必有妖!

我们仔细翻翻跟服务器的请求里有没有什么奇怪的东西.

这个就是请求的图片

看起来非常像密码对不对? 突破口一定在这副怪异的图片里.

该请求响应了一个 HTML, 里面全都是 span 标签, 我们尝试把 span 标签的 style 属性去除了(因为我分析应该和密文关系不大),

然后删除 # 这个字符, 看看最后还能剩下什么.

我意外的发现在 Chrome 开发工具的预览窗口可以直接过滤掉 style 属性, 这下就好办了.

我们把这些内容复制出来, 然后删除 #

已经可以看出来密码了

稍微整理下得到

1
Password:'7f593a77ef76c1f04d1cc5bc51aebf265559b965'    

通关

LEVEL 9

注意红框框住的地方

1
b'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAABmJLR0QA/wD/AP+gvaeTAAAE/klEQVR4nO3dwW7bOBSG0clg3v+VM7suhJSQTH4UW5yzbFxJcX4QNH15+fX9/f0PrPbv2w/A30mwSAgWCcEiIVgkBIvEfz/+69fX157bXxY7Lvd99NOxR7/R+MoLH/KQ33fG757KiEVCsEgIFgnBIvHz5P1i4feJM5PKmYnwzKeEzsKnGr85+/+CRiwSgkVCsEgIFolbk/eLhavY24yfeWa5fOGVH71XM2/shr+gEYuEYJEQLBKCReKTyXtnZgW8W1ufKW6ZudHMU73OiEVCsEgIFgnBInHW5P2iq4jvprp/xENuYMQiIVgkBIuEYJH4ZPL+1sx3/OKF91344plLdcUtGz4WGLFICBYJwSIhWCRuTd63beB85FGBysyS98Kyme6nY/v/gkYsEoJFQrBICBaJnyfvhxRsnPmhYaE/sS7oJiMWCcEiIVgkBIvE7m4zC1fAFy61P9Itpm9bW19Yj6TPO1sJFgnBIiFYJHZvWF04ie7KZg6xsPfOwvvevLIRi4RgkRAsEoJFYvfkfeakoRlvHU+6cEvq2CGV+L8YsUgIFgnBIiFYJL5+nHzNTLEX/t+ZJeBtnxIe3WjbGzvzXi15c4xYJASLhGCRECwSC2reZxa1u0nltqKatwpUZn7Bmcew8s6bBIuEYJEQLBKfdJvp9kbOVHmPrzzzf7ct8XfNZx5R8865BIuEYJEQLBI/l82E98u6P26b215s69V+cXhBjhGLhGCRECwSgkXi55X3biNlV8Y+vm83xe6WyxeW+nQFOb9jxCIhWCQEi4Rgkbi18t4tEG9bLn9rjfuQif/+v4IRi4RgkRAsEoJF4pNuM2OHrK2PXzy28DNEt7m3az6zhBGLhGCRECwSgkXik5X3t2aR25pQjv0R3xZcbPvy4BcjFgnBIiFYJASLxIKymf0Twzv/d2zbttLxfcfe+v5jzIZV3iRYJASLhGCRuFU281Zjk0dP9Uh3qYW2FSA9eoybjFgkBIuEYJEQLBILWkVuK+vedojoIeXz3UeoR/R55yCCRUKwSAgWiVutIruF6YW15zMe3XfbFwBvbRdwwirnEiwSgkVCsEj8PHm/eGuNe+bFM0ebztx3Yb3Ktt70C3/6ixGLhGCRECwSgkViQavI/ad33rnRX998pttVe2HlnYMIFgnBIiFYJD7ZsDp+8dghW0O7qX13suu2BjIXNqxyEMEiIVgkBIvErbKZR7q9oNt2ii58jJkbXXTfFox/qs87BxEsEoJFQrBILFh573SbXceXGtu2YfWtZjtLGLFICBYJwSIhWCTW93l/5Myl5/29We7c9y1q3jmIYJEQLBKCRWJBn/eFth3/9Nbmz7G3Osg/otsMbxIsEoJFQrBI3Orz3hl3Np+51MXC6Wp3OtIhh0MtqcAxYpEQLBKCRUKwSNzasHrIcUgXCyekC1+8cNZ8udRbJ1h9xohFQrBICBYJwSLxSbeZbhZ5SIX4wi8Auq6TF9u+WrjJiEVCsEgIFgnBIrG+VeRCj+by29qgd11uxkvtXdl+0YvGiEVCsEgIFgnBInH05H1m6fmQxooL6+W3ndk0puadNwkWCcEiIVgkPpm8dxPDmdORZirEH1lY29Md9LqwXOczRiwSgkVCsEgIFolbk/e3Wid2x7GO57bbTli96NbW928mMGKRECwSgkVCsEic1eedv4YRi4RgkRAsEoJFQrBICBYJwSLxP3LS5YIohRy8AAAAAElFTkSuQmCC'

这里是一串不知名的代码, 不过看开头非常像 PNG 格式的图片 Base64 编码后的格式

随便输一个密码看看有没有什么提示.

提示

1
Hint: png

我现在很确信这是一个 png 格式的图片 base64 编码后的代码

直接在网上还原该图片得到了一个二维码

继续识别一下该二维码则直接得到结果

1
f870f6a65d94a6f65a772b7f4734be95278e28fd

通关

LEVEL 10

本关给了一个加密函数的两个版本, js 和 python, 我比较熟悉 python 我们就来看下这个 python 函数的逻辑

1
2
def r(pwd, fr, to, by):
return ''.join([pwd[i] for i in range(fr, to, by)])

逻辑很简单, 该函数每次取出密码指定位的字符.

再看看输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> print(r(pwd, 0, 4, 1))
edac
>>> print(r(pwd, 4, 20, 2))
e76716c1
>>> print(r(pwd, 5, 30, 3))
1646561d3
>>> print(r(pwd, 5, 40, 4))
12b52d3fd
>>> print(r(pwd, 6, 40, 6))
711283
>>> print(r(pwd, 7, 8, 1))
e
>>> print(r(pwd, 10, 40, 3))
7bc5adfced
>>> print(r(pwd, 10, 40, 5))
7a6d85
>>> print(r(pwd, 15, 40, 6))
a2ffd
>>> print(r(pwd, 20, 40, 3))
61d3b5c

观察函数 r 的第三个参数, 我们发现最大值位 40, 该参数对应 range() 函数的第二位即 max.

于是我们可以知道 pwd 的长度位 40, 解码函数的逻辑就很清楚了.

创建一个长度 40 的数组, 然后将上述输出的结果依次填到数组的指定位置即可

开始编写解码函数

1
2
3
4
5
6
7
8
# 创建一个 40 长度的数组
_pwd = [ "" for _ in range(40) ]

#
def decrypt(text, fr, to ,by):
"""text 就是执行 r 函数的输出结果"""
for i, j in enumerate(range(fr, to, by)):
_pwd[j] = text[i]

然后依次执行

1
2
3
4
5
6
7
8
9
10
decrypt("edac", 0, 4, 1)
decrypt("e76716c1", 4, 20, 2)
decrypt("1646561d3", 5, 30, 3)
decrypt("12b52d3fd", 5, 40, 4)
decrypt("711283", 6, 40, 6)
decrypt("e", 7, 8, 1)
decrypt("7bc5adfced", 10, 40, 3)
decrypt("7a6d85", 10, 40, 5)
decrypt("a2ffd", 15, 40, 6)
decrypt("61d3b5c", 20, 40, 3)

拼接结果

1
print("".join(_pwd))

得到密码

1
edace17e62741b6ac51562a12ddff38cbfe53dcd

LEVEL 11

最后一关就有点难度了

因为你如果密码不正确的话是无法提交的, 也就是说没有任何的提示. 跟服务器也没有什么交互.

我们只能先取看看网页源码里有什么线索没有

果然发现了端倪, 源码里有一段 js 脚本, 看逻辑应该是处理提交密码的, 仔细分析下.

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
26
27
<script type="text/javascript">
$( document ).ready(function() {
$("#submit").on('click', function(e) {
e.preventDefault();
if (window.location.search.indexOf('challenge')!=-1) {debugger;}
pwd = $("#pwd").val();
ts = $("#pwd").data('ts')+'';
stmnt = getBiskuit('X-Odoo');
multi = true;
$(ts.substr(0,5).split('')).each(function( i , j) {
multi *= stmnt[parseInt(j) +1].charCodeAt(0);
});
if (parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0]) * parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1]) * stmnt.split("All").length == ts.slice(eval(""+''+""+ƒ(1<0)+""+"-"+''+""+ƒ(0<1)+"-"+ƒ(1>0)))) {
$.ajax("./70/"+ pwd, {
success: function (o) {
0===pwd.lastIndexOf(multi.toString().substr(1,4)+stmnt.substring(2,6),0)&&(
$.post('submit', {pwd: o, csrf_token:'ce1db5808eeed02fc22e46b80ef4651e39b7cd59o1687498808'}).always(function(){window.location.href='/jobs/challenge/next'})
);
},
error: function (o) {
console.error('To be or not to be... ');
}
});
}
});
});
</script>

有几个变量我们需要注意下

1
2
3
ts
stmnt
multi

我们直接在浏览器的控制台执行下面的代码看看有什么结果没有

1
2
3
4
5
6
ts = $("#pwd").data('ts')+'';
stmnt = getBiskuit('X-Odoo');
multi = true;
$(ts.substr(0,5).split('')).each(function( i , j) {
multi *= stmnt[parseInt(j) +1].charCodeAt(0);
});

然后我们往下看是一个条件判断

1
(parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0]) * parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1]) * stmnt.split("All").length == ts.slice(eval(""+''+""+ƒ(1<0)+""+"-"+''+""+ƒ(0<1)+"-"+ƒ(1>0))))

太长了我们可以拆分一下

1
2
3
parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0])
parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1])
stmnt.split("All").length

简单来讲就是上面三行的乘积需要等于下面该行的值

1
ts.slice(eval(""+''+""+ƒ(1<0)+""+"-"+''+""+ƒ(0<1)+"-"+ƒ(1>0)))

因为我们现在需要反推pwd这个值, 所以上面不涉及pwd变量的我们可以直接在控制台执行看看具体的值是多少

code value
stmnt.split(“All”).length 2
ts.slice(eval(“”+’’+””+ƒ(1<0)+””+”-“+’’+””+ƒ(0<1)+”-“+ƒ(1>0))) ‘70’

这里注意到变量类型不同, 但是我们也看到了这个条件匹配用的是 ==, 即不是严格模式! 所以我们可以把他们都当成数字来处理

接下来我们处理包含 pwd 变量的条件

我们把

1
parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0])

记作 a

1
parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1])

记作 b

则上述条件可以变为

1
a * b * 2 == 70

则可知 a b 的可选值表

a b
1 35
35 1
5 7
7 5

我们把

1
-(--([,,,undefined].join()).length)

在控制台里执行下, 然后得到值 -2

于是

1
2
3
parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0])
// 等价于
parseInt(pwd.slice(-2)[0])

于是我们可以分析出 a 表达式为 pwd 变量的倒数第二字符所代表的值

同理我们分析 b 表达式

1
2
3
parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1])
// 等价于
parseInt(pwd.slice(-2)[1])

b 表达式为 pwd 变量的倒数第一字符所代表的值

仔细观察 js 脚本还记不记得这句

1
pwd = $("#pwd").val();

意思是 pwd 取到的是一个字符串变量, 观察我们刚才的 a, b 取值表.

我们发现按下标从一个字符串里取值, 取到的一定是一个字符, 不可能为两个字符, 于是现在 a / b 取值表就变为

a b
5 7
7 5

也就是说 pwd 的最后两位一定是 5775

我们再往下看

1
0===pwd.lastIndexOf(multi.toString().substr(1,4)+stmnt.substring(2,6),0)

我们先看一下里面的具体值是什么.

1
2
3
multi.toString().substr(1,4)+stmnt.substring(2,6)
// 执行得到, 这个东西看起来非常的像密码
'4380Odoo'

查看一下 lastIndexOf 方法的说明

参考链接

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf

lastIndexOf() 方法返回调用String 对象的指定值最后一次出现的索引,在一个字符串中的指定位置 fromIndex处从后向前搜索。如果没找到这个特定值则返回-1 。

该方法将从尾到头地检索字符串 str,看它是否含有子串 searchValue。开始检索的位置在字符串的 fromIndex 处或字符串的结尾(没有指定 fromIndex 时)。如果找到一个 searchValue,则返回 searchValue 的第一个字符在 str 中的位置。str中的字符位置是从 0 开始的。

简单来讲, 就是想要

1
0 === pwd.lastIndexOf(multi.toString().substr(1,4)+stmnt.substring(2,6),0)

这个表达式成立, pwd 这个变量一定是 multi.toString().substr(1,4)+stmnt.substring(2,6) 所表示的值 4380Odoo 作为开头

又因为 pwd 的最后两位一定是 5775, 所以可以猜测密码应该为

1
2
4380Odoo57
4380Odoo75

这两个其中的一个

大功告成!

Congratulations !

基于元类

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
26
class Singleton(type):


def __init__(cls, *args, **kwargs):
""""""
cls.__instance = None
super().__init__(*args, **kwargs)


def __call__(cls, *args, **kwargs):
""""""
if cls.__instance is None:
cls.__instance = super().__call__(*args, **kwargs)
return cls.__instance
else:
return cls.__instance


class Spam(metaclass=Singleton):

...

s1 = Spam()
s2 = Spam()
print(id(s1))
print(id(s2))

基于 new 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton(object):

__instance = None

def __init__(self):
...


def __new__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls.__instance


class Spam(Singleton):

...

s1 = Spam()
s2 = Spam()
print(id(s1))
print(id(s2))

基于装饰器

用闭包实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def singleton(cls):
"""使用闭包的方式实现一个单例装饰器"""

__instance = {}

def _create(*args, **kwargs):
if cls not in __instance:
__instance[cls] = cls(*args, **kwargs)
return __instance[cls]

return _create


@singleton
class Spam(object):
...


s1 = Spam()
s2 = Spam()
print(id(s1))
print(id(s2))

用类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Singleton(object):

def __init__(self, cls):
self.__cls = cls
self.__instance = {}


def __call__(self, *args, **kwargs):
if self.__cls not in self.__instance:
self.__instance[self.__cls] = self.__cls(*args, **kwargs)
return self.__instance[self.__cls]


@Singleton
class Spam(object):
...


s1 = Spam()
s2 = Spam()
print(id(s1))
print(id(s2))

注意

Python的模块是天然的单例模式

在一个py文件中, 多次导入同一个模块, 这个模块也只有在第一次的时候被导入, 后续的该模块导入语句都不会再执行了

参考链接

https://developer.aliyun.com/article/653759

问题描述

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。

例如:

1
2
输入:s = "()"
输出:true
1
2
输入:s = "()[]{}"
输出:true
1
2
输入:s = "(]"
输出:false

即左右括号必须是闭合的。

解题思路

使用栈结构来处理这个问题非常简单。

我们从左到右依次扫描字符串的每一个字符,如果是([{左括号就入栈,如果是)]}右括号就从栈里弹出栈顶的元素,然后匹配该字符和栈顶元素是否是成对的。

例如扫描到字符(,然后栈顶元素是),则成对,否则不成对则返回False

扫描完字符串,如果栈里没有元素则说明括号都是成对的,匹配成功。

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
26
27
28
29
30
31
32
class Solution:
def isValid(self, s: str) -> bool:
# 字符串匹配成功的话其长度一定是偶数, 否则一定不成功
# 这里可以先检查长度, 可以省略之后的步骤
if len(s) % 2 != 0:
return False

# 辅助栈
_stack = []
# 对照表
_map = {
")": "(",
"]": "[",
"}": "{"
}
for _char in list(s):
if _char in "([{":
_stack.append(_char)
else:
# 当遇到右括号时, 从栈顶弹出元素检测是否
# 成对, 即是否闭合, 不成对则返回 False
if _stack:
if _char in _map:
_top = _stack.pop()
if _map[_char] != _top:
return False
else:
# 右括号溢出
return False
# 这里要判断栈里是否还有元素
# 有元素则说明该字符串匹配不成功
return len(_stack) == 0

原链接

https://leetcode.cn/problems/valid-parentheses/