题目介绍
首先是扫目录得到.git源码泄露,我们用Git_Extract
工具下载下来,有几个php文件和一个php的opcode,但是php文件都是经过so拓展加密,由于不会逆向,只能直接看着wp复现了,直接把师傅逆向好的代码拿来复现~~
index.php
<?php
if(!isset($_SESSION))
{
session_start();
}
include_once('func.php');
if(isset($_GET['username']))
{
$username = $_GET['username'];
$md5 = md5(get_identify().$username);
$admin = 0;
$token = encrypt($username.'|'.$admin.'|'.$md5);
$_SESSION['sign'] = $md5;
$_SESSION['token'] = $token;
}
showImage();
if(isset($_GET['token']) && isset($_GET['sign']))
{
$token = $_GET['token'];
$sign = $_GET['sign']; echo 'sign : '.$sign.'<br>'; echo 'token: '.$token.'<br>';
$info = explode('|', decrypt($token)); echo decrypt($token);
var_dump($info); if(count($info) == 3)
{ if(md5(get_identify().$info[0]) == $info[2])
{
$sign = $info[1];
$admin = $info[1];
}
else{
$admin = $info[1];
}
}
}
else{ if(isset($_SESSION['token']) && isset($_SESSION['sign']))
{ echo 'sign : '.$_SESSION['sign'].'<br>'; echo 'token: '.$_SESSION['token'].'<br>';
$token = $_SESSION['token'];
$sign = $_SESSION['sign'];
$info = explode('|', decrypt($token)); if(count($info) == 3)
{ if(md5(get_identify().$info[0]) == $info[2])
{
$sign = $info[1];
$admin = $info[1];
}else{
$admin = $info[1];
} echo '<br>'.$admin;
}
}
} if(isset($admin) && $admin == 3)
{
$_SESSION['auth'] = 'admin'; echo "<a href='admin.php'>Admin</a>";
}
func.php
<?php/**
* Created by PhpStorm.
* User: meizj
* Date: 2018/2/2
* Time: 下午9:25
*/include "class.php";
define("KEY","8690475385984657");
define("method","aes-128-cfb");
define("BS",16);
define("IDENTIFY","9850375038");
function get_token(){
$token = '';
for($i=0;$i<16;$i++){
$token .= chr(rand(1,255));
} return $token;
}
function enc($s){
$token = get_token();
$code1 = openssl_encrypt(string($s),method,key,OPENSSL_RAW_DATA,$token);
$code2 = base64_encode(base64_encode($token."-".$code1)); return $code2;
}
function dec($s){ if($cc = base64_decode(base64_decode($s)))
{ if($iv = substr($cc,0,16))
{ if($d = substr($cc,17))
{ if($s = openssl_decrypt($d, method, key, OPENSSL_RAW_DATA,$iv))
{ return $s;
} else
die("error");
} else
return 0;
} else
return 0;
} else
return 0;
}
function uploadImage(){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed");
}
$AllowedType = array( "png", "gif", "jpg"
);
$filename = $_FILES['file']['name'];
$filesize = $_FILES['file']['size']; if($filesize > 1000000){ exit("Too large");
}
$fileext = substr($filename, strrpos($filename, '.')+1); if(in_array($fileext,$AllowedType)){
$file = "thumbs/images/".md5(time()."admin").".".$fileext; if(file_exists($file)){ exit("File existed already");
}else{
move_uploaded_file($_FILES['file']['tmp_name'],$file);
}
}else{ exit("Not Allowed Ext");
}
}
function viewImage($name){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed");
} new ImageView($name);
}
function showImage(){
$obj = new Home("thumbs/images/");
$obj->showImg();
}function to($str) { return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS));
}function re($str) { return substr($str, 0, -ord(substr($str, -1, 1)));
}function getkey(){ return KEY;
}function get_identify(){ return IDENTIFY;
}function encrypt($str){
$key = getkey();
srand(time() / 300);
$token = get_token();
$cipher = bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, to($str), MCRYPT_MODE_CFB, $token));
return base64_encode($cipher);
}
function decrypt($str){
$decode = base64_decode($str);
$key = getkey();
srand(time() / 300);
$token = get_token();
$bin = hex2bin($str);
$plain = re(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,$bin , MCRYPT_MODE_CFB, $token)); return $plain;
}
我们可以看到,我们传入一个username参数就会得到一个sign和一个token,其中加密是使用了cfb加密,这种加密方法可以用重放攻击,后面再介绍这个
关键代码
$md5 = md5(get_identify().$username);
$admin = 0;
$token = encrypt($username.'|'.$admin.'|'.$md5);
$_SESSION['sign'] = $md5;
$_SESSION['token'] = $token;
加密的形式是$username.'|'.$admin.'|'.$md5
其中$admin
为0,根据代码我们知道,$admin
必须为3,我们才能访问到admin.php
那么我们如何构造admin的值呢
cfb重放攻击
cfb的加密方式和cbc加密不同,当密文中某这个值改变后,这个值后面一个组不能解密,其它正常
我们举一个例子
from Crypto.Cipher import AES
from os import urandom
from Crypto.Util.strxor import strxor
class AES_CFB:
def __init__(self):
self.key = urandom(16)
self.iv = urandom(16)
def encrypt(self, plain):
aes = AES.new(self.key, AES.MODE_CFB, self.iv)
return aes.encrypt(plain)
def decrypt(self, cipher):
aes = AES.new(self.key, AES.MODE_CFB, self.iv)
return aes.decrypt(cipher)
plain ='1'*32
aes = AES_CFB()
cipher = aes.encrypt(plain)
print aes.decrypt(cipher)
ct = cipher[:3]+strxor(strxor(cipher[3:9], '1'*6), '3'*6)+cipher[9:]
print aes.decrypt(ct)
我们原来的明文是'1'*32
我们的输出结果为
可以看见,我们要更改的明文第一个值为3,剩下10个1没有改变
10=32-6-16
(其中6代表我们更改的值,但是只有第一位我们能决定是什么值,16代表下一组密文被更改)
那么我们假设注册一个username为
xxxxxxxxxxxxxxx
那么我们我们的的明文则为
xxxxxxxxxxxxxxx|0|d30ef4a963f6cadc8a453977ccbbc626
我们观察到最后sign的长度刚好为32
我们先拓展一下token
$user|$admin|$md5
变为
$user|$admin|$md5$user|$admin|$md5
最后的值为
xxxxxxxxxxxxxxx|0|d30ef4a963f6cadc8a453977ccbbc626xxxxxxxxxxxxxxx|0|d30ef4a963f6cadc8a453977ccbbc626
由于index.php
会校验info[0]
和info[2]
,所以我们必须保持xxxxxxxxxxxxxxx
和d30ef4a963f6cadc8a453977ccbbc626
分别在第一部分和第三部分
这儿再利用一个php的弱类型我们知道在php中3aaaaaaaaa==3
而我们前面也说了,我们刚好能控制第一个值,所以我们将第一个值改为3就行了
我们就要决定改多长的长度,而改了后后面恰好又要留下一个分组
我们这儿贴出代码
from Crypto.Cipher import AES
from os import urandom
from Crypto.Util.strxor import strxor
import base64
plain = 'xxxxxxxxxxxxxxx|0|d30ef4a963f6cadc8a453977ccbbc626' # 最原始的明文
token = 'ZjVhYWI3NjA3ZTZiOGM4Y2YyMWUxOTRiNzhiMzFmNzkwYjYyMjAzYjZlZTgxMDAwNTY0ZDMyNzlkYTFiODk3YmQ1MDFiYjk2NzkyYmJhMzgwYzQ4OTgzMTlhYWQ5Nzk0OGExOGEwM2VkZDU4NjIwYzdhMTIxNmQ1ODY2OWI3YzM=' #对应的密文
token = base64.b64decode(token)
cipher = token.decode("hex")
ct = cipher[:16]+strxor(strxor(cipher[16:50], '0'*34), '3'*34)+cipher # 我们从第17个字符改,即0,改的长度为34,则后面的异或的长度也必须是34,而且第一个异或的值必须明文第十七的字符,第二个异或是我们想要更改的第一个值,由于admin要求是3,所以我们填3就行,最后我们+cipher是拓展我们的token,这样我们就更改了0|d30ef4a963f6cadc8a453977ccbbc626,下一组密文也是乱码即xxxxxxxxxxxxxxx|,这样剩下的一个0无关影响,因为我们只剩下两个||,刚好符合要求~~
ct = "".join(ct).encode('hex')
print(base64.b64encode(ct))
这样传参sign,token就能是admin,然后就能访问admin.php
了,剩下的比较简单,这儿就不做过多的说明了~~
非预期
这道题目还有一个非预期,由于源码中泄露了密匙,而且随机种子五分钟变一下,所以我们直接照这代码运行一下就行了~~
额外的东西
这儿再记录一个东西,以前不知道了,主要是自己太菜了,大佬请略过
srand(time() / 300)
这儿是给rand设定随机种子,只要设定了种子
rand(1,255)
的顺序也是固定不变的,虽然有范围限制