0%

在使用 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/

问题描述

记录一次奇怪的 DEBUG 过程

事情的起因是我在搭建一个前端项目, 第一次打开可以, 但是如果再刷新页面则会报错.

仔细查看我的代码, 发现没有什么问题, 最后跟踪错误到了vue-router

控制台报错

1
2
3
4
Uncaught (in promise) TypeError: api.now is not a function
at vue-router.esm-bundler.js?v=44d7056b:2545:31
at triggerAfterEach (vue-router.esm-bundler.js?v=44d7056b:3166:13)
at vue-router.esm-bundler.js?v=44d7056b:3069:13

继续跟踪

1
2
3
4
5
6
7
8
9
10
11
api.addTimelineEvent({
layerId: navigationsLayerId,
event: {
title: 'End of navigation',
subtitle: to.fullPath,
time: api.now(), <--
data,
logType: failure ? 'warning' : 'default',
groupId: to.meta.__navigationId,
},
});

如果出错的地方在业务逻辑, 则可以很容易的找出错误原因, 但是这个地方报错, 让我毫无头绪.

Google …

没想到还真的找到了解决方案, 而且还就在该文章发布时间的前半个月(还热乎着…)

解决方案

That’s from the Vue devtools plugin. And only happens if you are still on the 6.0 beta of Vue devtools.

Happened to me today when checking in another browser that still was on the 6.0 beta version instead of the stable one we released recently.

Solution: remove the beta, upgrade to the stable release

大意是说, 如果你开发时使用了 Vue Devtools 这个插件, 并且还是 6.0 beta 版本时, 就会发生这个问题.

解决方案也很简单, 卸载这个测试版, 安装稳定版

https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd

现在想想, 这个插件我一年前装的, 然后就没管过, 我不清楚它会不会自动更新, 但是确实在我意想不到的地方给我挖了个大坑….

总结

这个事情总结出什么经验呢?

BUG 有时候就是发生的这么毫无逻辑…让人捉摸不定

参考链接

https://github.com/vuejs/router/issues/1338

前言

在 Python 中经常会做的一件事就是将某个对象序列化, 序列化有很多种方式, JSON 是最常用的其中一种(方便和前端交换数据).

但是一个明显的问题是, Python 标准库json, 仅仅能dumps其内置的基本数据类型, 例如 string, integer, boolean, list, dict …

并且 JSON 作为一种数据交换格式, 有其固定的数据类型.
有时候我们需要将我们编写的一个类序列化, 此时该怎么办?

JSON 的介绍可以参考其官网说明

序列化非标准类型时遇到的问题

如果 json.dumps 一个非标准类型(例如一个我们编写的类)会发生什么事?

我们先定义一个类Person

1
2
3
4
class Person:

def __init__(self):
self.name = "naonao"

尝试用 json 序列化

1
2
3
4
5
6
7
import json

# 创建一个实例
p = Person()

# 尝试序列化
json.dumps(p)

毫不意外的报错了

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
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [76], in <module>
4 p = Person()
6 # 尝试序列化
----> 7 json.dumps(p)

File /opt/conda/lib/python3.9/json/__init__.py:231, in dumps(obj, skipkeys, ensure_ascii, check_circular, allow_nan, cls, indent, separators, default, sort_keys, **kw)
226 # cached encoder
227 if (not skipkeys and ensure_ascii and
228 check_circular and allow_nan and
229 cls is None and indent is None and separators is None and
230 default is None and not sort_keys and not kw):
--> 231 return _default_encoder.encode(obj)
232 if cls is None:
233 cls = JSONEncoder

File /opt/conda/lib/python3.9/json/encoder.py:199, in JSONEncoder.encode(self, o)
195 return encode_basestring(o)
196 # This doesn't pass the iterator directly to ''.join() because the
197 # exceptions aren't as detailed. The list call should be roughly
198 # equivalent to the PySequence_Fast that ''.join() would do.
--> 199 chunks = self.iterencode(o, _one_shot=True)
200 if not isinstance(chunks, (list, tuple)):
201 chunks = list(chunks)

File /opt/conda/lib/python3.9/json/encoder.py:257, in JSONEncoder.iterencode(self, o, _one_shot)
252 else:
253 _iterencode = _make_iterencode(
254 markers, self.default, _encoder, self.indent, floatstr,
255 self.key_separator, self.item_separator, self.sort_keys,
256 self.skipkeys, _one_shot)
--> 257 return _iterencode(o, 0)

File /opt/conda/lib/python3.9/json/encoder.py:179, in JSONEncoder.default(self, o)
160 def default(self, o):
161 """Implement this method in a subclass such that it returns
162 a serializable object for ``o``, or calls the base implementation
163 (to raise a ``TypeError``).
(...)
177
178 """
--> 179 raise TypeError(f'Object of type {o.__class__.__name__} '
180 f'is not JSON serializable')

TypeError: Object of type Person is not JSON serializable

我们仔细观察报错信息, 提示 Person 不是一个 JSON 序列化对象

1
TypeError: Object of type Person is not JSON serializable

那么问题来了, 我们如何把各种各样的 Python 对象序列化成 JSON 格式?

Google 和查阅官方文档后你会发现 dumps 方法提供了一个 cls 参数, 我们可以自己编写一个序列化类, 告诉该他该如何dumps这个对象.

解决方案

例如

1
2
3
4
5
6
7
8
9
class PersonEncoder(json.JSONEncoder):

# 重写该方法, 告诉 json
# 如何 dumps 指定的对象
def default(self, _object):
if isinstance(_object, Person):
# 这里编写如何序列化 Person
# 对象的代码
return dict(name=_object.name)

我们再尝试一下

1
json.dumps(p, cls=PersonEncoder)

得到结果

1
'{"name": "naonao"}'

成功了, 完美的解决了问题.

有点遐思

但是现在有一个问题, 如果序列化少量类, 我们只需要在default这个方法下编写少量的代码即可. 但通常一个应用不可能仅仅只有几个少量的类, 类多了怎么办?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def default(self, _object):
if isinstance(_object, Person):
...
elif isinstance(_object, Person1):
...
elif isinstance(_object, Person2):
...
elif isinstance(_object, Person3):
...
elif isinstance(_object, Person4):
...
elif isinstance(_object, Person5):
...
elif isinstance(_object, Person6):
...
elif isinstance(_object, Person7):
...

这种写法, 一点也不优雅!

我们需要一种更优雅的解决方案

更优雅的解决方案

该函数可以解决这个问题

1
from functools import singledispatch

详细说明可以看官方文档

我们先创建两个类

1
2
3
4
5
6
7
8
9
10
11
12
13
import json
from functools import singledispatch

class Person:

def __init__(self):
self.name = "naonao"
self.age = 18

class Animal:

def __init__(self):
self.name = "dog"

创建两个实例

1
2
person = Person()
animal = Animal()

接下来我们可以这样定义序列化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@singledispatch
def serialize(_object):
return json.dumps(_object)

@serialize.register(Person)
def _(_object):
return dict(name=_object.name,
age=_object.age)

@serialize.register(Animal)
def _(_object):
return dict(name=_object.name)

class CustomJSONEncoder(json.JSONEncoder):

def default(self, _object):
return serialize(_object)

尝试一下

1
2
3
4
5
data = {
"person": person,
"animal": animal
}
json.dumps(data, cls=CustomJSONEncoder)

完美解决

1
'{"person": {"name": "naonao", "age": 18}, "animal": {"name": "dog"}}'

接下来有新的类(或是数据类型)进行序列化时, 我们仅仅需要参照这个格式

1
2
3
4
5
6
                    # 目标类
@serialize.register(Person)
def _(_object):
# 如何序列化这个对象
return dict(name=_object.name,
age=_object.age)

编写对应的解析器即可. 这样可比 if else ...循环嵌套可读性高了不知多少倍!

参考链接

https://juejin.cn/post/6844903510543171592

https://docs.python.org/zh-cn/3/library/functools.html

http://www.json.org/json-zh.html

1
2
3
4
5
6
7
8
class Person:
...

# 实例
p = Person()

# 实例的类
p.__class__

今天有个工作场景是一个注册函数需要执行一次注册任务, 但是该函数仅仅只能在初始化的时候执行一次注册任务, 即该函数只能执行一次.

如何实现这个逻辑?

1
2
3
4
5
6
7
8
9
10
def _register():
print("register ...")


_register()
_register()
_register()

# 多次调用也只能输出一次
>>> register ...

基于类的单例模式

… 略, 想起来再写

基于闭包

我们还可以使用闭包来实现这个逻辑.

维基百科的闭包解释

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

看起来非常的绕, 笔者的简单理解就是函数打包了一份本地环境, 并且通常来将, 闭包的内部变量对于外界是完全隐藏的(但是也有办法访问到)

基于闭包实现上述逻辑

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

# 这里我们在函数内部定义了一个
# 内部变量, 表示该函数是否已经
# 执行过
_loaded = False

def _load():
# 这里写该函数的具体执行逻辑
print("fisrt run, loading ...")
print("loaded")
# 当逻辑被执行一次以后
# 我们需要将设置 _loaded = True
# 用来表示该函数已经被执行过一次
# nonlocal 表示该变量不是本地变量
nonlocal _loaded
_loaded = True

def _run():
if not _loaded:
_load()

return _run

这样我们就实现了一个只能执行一次的函数

执行

1
2
3
f = _register()
f()
f()

可以发现只执行了一次逻辑

1
2
fisrt run, loading ...
loaded