介绍
Odoo Challenge 挑战赛是一个很有意思的项目, 起源于一次面试时, HR发来个链接说测试一下水平.
好了, 开始挑战
LEVEL 1
先随便输一个密码, 点 Check Answer, 很不意外的密码错误, 不过这时候有提示, 可以根据提示的线索来找到密码.
看到提示
1 | Hint: if form['the_password'] == form['pwd']: return True |
看看网页的源码, 猜测应该在里面能找到.
部分源码
1 | <form method="POST" action="/jobs/challenge/submit" role="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 | admin |
诸如此类的弱口令发现都没有用。
最后我写了一个脚本, 从 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 | // JS |
看来一下逻辑, tmp 变量存储的应该是 idx 字符串的索引, 我们把索引对应的字符取出来即可.
可以用 Python 实现一个简单的解码函数
1 | idx = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
得到密文
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/ |
请求头也有一个奇怪的信息
name | value |
---|---|
It-Is-The-Part-1-Of-Url | “/jobs/challenge/ebe/ |
这时候我们可以分析, 这个 URL 被分为了两部分, 放到一起看一下
1 | /jobs/challenge/<part1>/1d1/prime.json |
那么完整的 URL 应该是
1 | /jobs/challenge/ebe/1d1/prime.json |
我们尝试访问这个 URL
得到一串 JSON
1 | { |
翻译一下, 大概意思是, 把这个数组里的非素数删除了, 然后将剩余的数字右移x位(也许是位运算?), 然后就得到了密码
x 位指的是应该是 pi 的第 222 位的数字
至此, 我们梳理一下完整的逻辑, 把上面给定的数组里的非素数删除, 然后找到 pi 的第 222 位的值 x, 然后将数组中剩余的数字按位运算右移 x 位.
网上找一个高效的判断是否位素数的函数
1 | def is_prime(x: int) -> bool: |
查一下 pi 的第 222 位是几, 这里可以自己写函数计算也可以直接在网上查, 这里仅仅需要知道该值即可, 所以我选择了直接在网上查
直接在网站
1 | http://pai.babihu.com/ |
查的 pi 的 222 位为 8
还记的最开始的提示吗?
1 | Hint: ascii |
这里运算完的应该是 ascii 码, 我们计算出对应的字符就得到了最后的密文, 至此我们开始编写解码函数
1 |
|
解密得密码
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 | def r(pwd, fr, to, by): |
逻辑很简单, 该函数每次取出密码指定位的字符.
再看看输出结果
1 | print(r(pwd, 0, 4, 1)) |
观察函数 r 的第三个参数, 我们发现最大值位 40, 该参数对应 range() 函数的第二位即 max.
于是我们可以知道 pwd 的长度位 40, 解码函数的逻辑就很清楚了.
创建一个长度 40 的数组, 然后将上述输出的结果依次填到数组的指定位置即可
开始编写解码函数
1 | # 创建一个 40 长度的数组 |
然后依次执行
1 | decrypt("edac", 0, 4, 1) |
拼接结果
1 | print("".join(_pwd)) |
得到密码
1 | edace17e62741b6ac51562a12ddff38cbfe53dcd |
LEVEL 11
最后一关就有点难度了
因为你如果密码不正确的话是无法提交的, 也就是说没有任何的提示. 跟服务器也没有什么交互.
我们只能先取看看网页源码里有什么线索没有
果然发现了端倪, 源码里有一段 js 脚本, 看逻辑应该是处理提交密码的, 仔细分析下.
1 | <script type="text/javascript"> |
有几个变量我们需要注意下
1 | ts |
我们直接在浏览器的控制台执行下面的代码看看有什么结果没有
1 | ts = $("#pwd").data('ts')+''; |
然后我们往下看是一个条件判断
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 | parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0]) |
简单来讲就是上面三行的乘积需要等于下面该行的值
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 | parseInt(pwd.slice(-(--([,,,undefined].join()).length))[0]) |
于是我们可以分析出 a 表达式为 pwd
变量的倒数第二字符所代表的值
同理我们分析 b 表达式
1 | parseInt(pwd.slice(0 - - - 1 - - - - - 1 - - - - 0)[1]) |
b 表达式为 pwd
变量的倒数第一字符所代表的值
仔细观察 js 脚本还记不记得这句
1 | pwd = $("#pwd").val(); |
意思是 pwd
取到的是一个字符串变量, 观察我们刚才的 a, b 取值表.
我们发现按下标从一个字符串里取值, 取到的一定是一个字符, 不可能为两个字符, 于是现在 a / b 取值表就变为
a | b |
---|---|
5 | 7 |
7 | 5 |
也就是说 pwd
的最后两位一定是 57
或 75
我们再往下看
1 | 0===pwd.lastIndexOf(multi.toString().substr(1,4)+stmnt.substring(2,6),0) |
我们先看一下里面的具体值是什么.
1 | multi.toString().substr(1,4)+stmnt.substring(2,6) |
查看一下 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
的最后两位一定是 57
或 75
, 所以可以猜测密码应该为
1 | 4380Odo |
这两个其中的一个
大功告成!