由于本人打比赛的时候还是菜鸡一个,只做了个签到题,昨天刚好刷到了,就想着本地复现一下。
也是记录一下一个看似是web实则是crypto的题,难度我就不多说了,往下看就知道了。
访问地址
这是首页界面,我们现在随便输个值可以看到有两个参数
手动随便爆破或者BurpSuite跑一下可以发现当password2=5时会回显源码
本地copy下来,这里我只放关键代码
<?php
function MyHashCode($str)
{
$h = 0;
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$hash = intval40(intval40(40 * $hash) + ord($str[$i]));
}
return abs($hash);
}
function intval40($code)
{
$falg = $code >> 32;
if ($falg == 1) {
$code = ~($code - 1);
return $code * -1;
} else {
return $code;
}
}
function Checked($str){
$p1 = '/ISCC/';
if (preg_match($p1, $str)){
return false;
}
return true;
}
function SecurityCheck($sha1,$sha2,$user){
$p1 = '/^[a-z]+$/';
$p2 = '/^[A-Z]+$/';
if (preg_match($p1, $sha1) && preg_match($p2, $sha2)){
$sha1 = strtoupper($sha1);
$sha2 = strtolower($sha2);
$user = strtoupper($user);
$crypto = $sha1 ^ $sha2;
}
else{
die("wrong");
}
return array($crypto, $user);
}
error_reporting(0);
$user = $_GET['username'];//user
$sha1 = $_GET['sha1'];//sha1
$sha2 = $_GET['//sha2sha2'];
//see me can you
if (isset ($_GET['password'])) {
if ($_GET['password2'] == 5){
show_source(__FILE__);
}
else{
//Try to encrypt
if(isset($sha1) && isset($sha2) && isset($user)){
[$crypto, $user] = SecurityCheck($sha1,$sha2,$user);
if((substr(sha1($crypto),-6,6) === substr(sha1($user),-6,6)) && (substr(sha1($user),-6,6)) === 'a05c53'){//welcome to ISCC
if((MyHashcode("ISCCNOTHARD") === MyHashcode($_GET['password']))&&Checked($_GET['password'])){
include("flag.php");
echo $flag;
}else{
die("就快解开了!");
}
}
else{
die("真的想不起来密码了吗?");
}
}else{
die("密钥错误!");
}
}
}
mt_srand((microtime() ^ rand(1, 10000)) % rand(1, 1e4) + rand(1, 1e4));
?>
可以看到有5个if判断,绕过后即可获得flag
先看第一个,就是让我们GET传参一个password;第二个if就是当password2=5时回显源码,否则进入下一个if判断;第3个if是让我们GET传入sha1、"sha2"、username三个参数,至于这个sha2为什么要特别标注,这里我埋下个伏笔;第4个if是先通过 SecurityCheck($sha1,$sha2,$user) 这个函数将我们GET传入的sha1、"sha2"、username三个参数进行处理,最后返回的结果是将$sha1,$sha2的值进行按位异或得到 $crypto参数和我们GET传入的username的值赋给 $user,然后判断 $user 经过sha1加密后的后六位字符是否等于 'a05c53' ,$crypto 经过 sha1加密后的后六位字符是否等于$user 经过sha1加密后的后六位字符,也就是 'a05c53';最后一个if就是判断我们GET的password这个参数经过MyHashcode()函数后是否等于 MyHashcode("ISCCNOTHARD"),并且要经过Checked()函数的过滤,也就是不能含有 'ISCC'。
简单梳理过后我们可以发现,现在 $user 的值是最好确定的,可以写个py脚本爆破出来
import hashlib
for num in range(10000,9999999999):
res = hashlib.sha1(str(num).encode()).hexdigest()
if res[-6:] == "a05c53":
print(str(num))
break
爆破出来的结果为 14987637,也就是GET的username=14987637,现在$user的值确定了,根据上面我们对第3个if的分析,我们可以让$crypto的值也等于14987637,$crypto和$user值相等,他们的后六位自然相等。
需要注意这个函数,他会判断sha1是否是小写字母,sha2是否是大写写字母,是就会将sha1变成全大写字母,sha2变成全小写字母,这里只用注意传参值就行,因为异或我试过, a^P 和 A^p的结果是一样的。
function SecurityCheck($sha1,$sha2,$user){
$p1 = '/^[a-z]+$/';
$p2 = '/^[A-Z]+$/';
if (preg_match($p1, $sha1) && preg_match($p2, $sha2)){
$sha1 = strtoupper($sha1);
$sha2 = strtolower($sha2);
$user = strtoupper($user);
$crypto = $sha1 ^ $sha2;
}
else{
die("wrong");
}
return array($crypto, $user);
}
同样写个py脚本,我的脚本是爆出所以字母异或后的值是14987637这几个数字中的一个
for sha1 in 'abcdefghijklmnopqrstuvwsyz':
for sha2 in 'ABCDEFGHIJKLMNOPQRSTUVWSYZ':
sha = chr(ord(sha1) ^ ord(sha2));
#print(sha)
if sha in '14987637':
print(sha1, sha2, sha)
将得到的结果拼接能得到14987637就行,我取的是sha1=aacaaaaa,sha2=PUZYVWRV
验证一下
那么接下来我就要说说我分析时留下的那个伏笔了,我把源码copy下来时用在线编译和记事本时
好像并没有什么问题,但是我copy到phpstorm时却发现了一个很有意思的东西
我们会发现sha2好像有地方乱码了,一开始我以为是显示问题,后来通过下面的文章我了解到了这是特殊的unicode编码
https://mp.weixin.qq.com/s/lo2AiEloACLtCn2Ncle33A
根据文章,我上面贴了一下浏览器页面,细心的朋友会发现,//sha2 的sha2的颜色是不是红一点呢?也就是说真正的 "sha2" 参数其实是 "$a ='sha2';//sha2" 进行urlencode编码后的值,因此"sha2" 参数真正的参数名是
%E2%80%AE%E2%81%A6%2F%2Fsha2%E2%81%A9%E2%81%A6sha2
因此需要GET传参 sha1=aacaaaaa&%E2%80%AE%E2%81%A6%2F%2Fsha2%E2%81%A9%E2%81%A6sha2=PUZYVWRV
说实话做到这里我真的惊叹了很久。。。之前我看了很多WP都没有提到这一点,每次看他们的payload的时候都不知道为什么sha2要这么传。。。
现在终于到了最后一个if的绕过,根据上面的分析,我们现在重点看这三个函数
function MyHashCode($str)
{
$h = 0;
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$hash = intval40(intval40(40 * $hash) + ord($str[$i]));
}
return abs($hash);
}
function intval40($code)
{
$falg = $code >> 32;
if ($falg == 1) {
$code = ~($code - 1);
return $code * -1;
} else {
return $code;
}
}
function Checked($str){
$p1 = '/ISCC/';
if (preg_match($p1, $str)){
return false;
}
return true;
}
intval40它的作用是将一个64位整数转换为40位整数。具体实现是通过判断最高位是否为1来确定是否需要进行补码操作,然后将结果返回。其中,如果最高位为1,则需要进行补码操作,将其转换为相应的负数。最后,返回40位整数的值。这是我对代码做点修改方便看是否要取补码,yes是取,no是不取。
MyHashCode($str)这个函数就是将$hash(初始值为0)经过两次intval40后在加上传入字符串对应每个字符的ASCII码值,我们可以先跑一下代码方便理解
可以看到MyHashcode("ISCCNOTHARD") 的值是取其结果的最后一次,而 ISCC 这4个字符在intval40后都没取补码,现在我要让
MyHashcode("ISCCNOTHARD") === MyHashcode($_GET['password']),并且password里面不能有ISCC,那么我们是否可以用某些字符串代替ISCC,从而绕过呢?
也就是说我们现在给password的值在经过MyHashcode()后取最后一次结果的值要等于MyHashcode("ISCCNOTHARD") 的值是取其结果的最后一次结果的值,也就是787668828277355348。我们知道$hash的值一开始是0,经过MyHashcode的第一次循环后就是输入参数的第一个字符的ASCII码值,第二次循环后就是输入参数的第一个字符的ASCII码值乘以40后再加上第二个字符的ASCII码值,现在有个思路就是让其$hash的值在经过循环后可以等于73,因为I经过一次循环后的值是73,简单来说就是让
MyHashcode("ISCCNOTHARD") === MyHashcode("代替I的字符SCCNOTHARD"),简单点想
我们让ASCII码为1的字符为第一个字符,这样经过MyHashcode的第一次循环后$hash的值就是1,第二次循环后$hash的值就是1*40+第二个字符的ASCII码值(值是33,也就是 !号),这样$hash的值就等于73了。也就是 CET传参 password=%01!SCCNOTHARD //ascii为1的字符是非可见 字符,经过url编码后为%01
这是我比较能理解的思路,网上别人也有对应的脚本,但我不明白为什么那样写,我把脚本贴下面了,我不理解的就是为什么一定要是 'IR'然后取拼接,我试过用'IA'、'IB'等都爆不出来,下面脚本最终爆出来是 IRkCNOTHARD。
<?php
function intval40($code)
{
$falg = $code >> 32;
if ($falg == 1) {
$code = ~($code - 1);
return $code * -1;
} else {
return $code;
}
}
function MyHashCode($str)
{
$h = 0;
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$hash = intval40(intval40(40 * $hash) + ord($str[$i]));
}
return abs($hash);
}
function Checked($str){
$p1 = '/ISCC/';
if (preg_match($p1, $str)){
return false;
}
return true;
}
$a="abcdefghijklmnopqrstuvwxyz";
echo MyHashCode("ISCCNOTHARD");
for($i;$i<strlen($a);$i++) {
echo "<br>";
echo MyHashCode("IS".$a[$i]."CNOTHARD");
if(MyHashCode("IR".$a[$i]."CNOTHARD")===MyHashCode("ISCCNOTHARD")){
echo "IR".$a[$i]."CNOTHARD";
}
}
最终payload:
?password=%01!SCCNOTHARD&password2=1&username=14987637&sha1=aacaaaaa&%E2%80%AE%E2%81%A6%2F%2Fsha2%E2%81%A9%E2%81%A6sha2=PUZYVWRV
得到flag (太不容易了!!!!)
之后有时间我会继续复现 ISCC2023的 老狼老狼几点了和 where_is_you_love (我只复现POP链)